QC required
diff --git a/html/wp-sections.js b/html/wp-sections.js
new file mode 100644
index 0000000..2673ea5
--- /dev/null
+++ b/html/wp-sections.js
@@ -0,0 +1,94 @@
+/* Work package section toggles — CR-006 / T5.5.
+ ---------------------------------------------------------------------------
+ The structural fix behind most of the removal requests in the plan. Rather
+ than deleting fields globally, each project turns on only the sections it
+ uses: it is what lets Micron drop Kitting and Assets while another project
+ keeps them, and it is why CR-002 and CR-016 are toggles rather than deletions.
+
+ THE ONE RULE: toggling a section OFF never deletes anything. It stops the
+ section rendering — in the creation form, in the detail view and in the PDF
+ export — and that is all. Whatever was captured stays on the package, and
+ toggling back on shows it again, intact. Everything in this file is about
+ what is DISPLAYED; nothing here writes to a package.
+
+ This list lives in its own file because three surfaces read it and they must
+ not drift: the SOP wizard renders the toggles, the creator applies them to
+ its form and its printed output, and a work package's detail view honours
+ them. A fourth copy is how "Assets is off" and "Assets is off, except in the
+ export" happen.
+
+ IDS ARE PERMANENT. They are written into every SOP that has ever been saved,
+ so renaming one silently turns that section back on for every existing
+ project. Change `label` freely; never change `id`.
+*/
+(function (window) {
+ 'use strict';
+
+ var LIST = [
+ { id: 'general', label: 'General Information',
+ note: 'Holds the WP number, subject and type. Turning this off leaves nothing to identify a package by — it is listed for completeness, not as a suggestion.' },
+ { id: 'location', label: 'Location',
+ note: 'Where the work happens. CR-004 gives this its own structured fields; today it is the location field inside General Information.' },
+ { id: 'scope', label: 'Scope of Work',
+ note: 'The ordered steps the crew performs, and the labour estimate.' },
+ { id: 'assets', label: 'Assets',
+ note: 'Asset tags and controls.dev links. Off for Micron EUV — the content duplicates the database Clinton’s team maintains (CR-016).' },
+ { id: 'materials', label: 'Materials',
+ note: 'The bill of materials that feeds kitting.' },
+ { id: 'kitting', label: 'Kitting',
+ note: 'Kitting status, warehouse owner and MIMO. Off for Micron EUV, which is not kitting today (CR-009).' },
+ { id: 'drawings', label: 'Drawings and Attachments',
+ note: 'Drawing references and attachment links.' },
+ { id: 'constraints', label: 'Constraints',
+ note: 'Release-readiness items. A package cannot be issued while one is open.' },
+ { id: 'qaqc', label: 'QA/QC',
+ note: 'Quality requirements, photo standard and hold points.' },
+ { id: 'closeout', label: 'Closeout',
+ note: 'Actual hours, installed quantity, redlines and lessons learned. Actual Hours stays here — its removal was proposed and rejected (CR-017).' },
+ ];
+
+ var IDS = LIST.map(function (s) { return s.id; });
+
+ function defaults() {
+ // A new SOP has everything on. A project opts OUT of what it does not use;
+ // it does not have to discover and opt in to what it does.
+ var out = {};
+ IDS.forEach(function (id) { out[id] = true; });
+ return out;
+ }
+
+ /* Fill in anything a stored SOP does not mention.
+
+ This is what makes adding an eleventh section safe: every SOP saved before
+ it existed says nothing about it, and "says nothing" has to mean ON. The
+ alternative — absent meaning off — would switch a brand-new section off for
+ every project in the estate the moment it shipped. */
+ function normalize(stored) {
+ var out = defaults();
+ if (stored && typeof stored === 'object') {
+ IDS.forEach(function (id) {
+ if (Object.prototype.hasOwnProperty.call(stored, id)) out[id] = stored[id] !== false;
+ });
+ }
+ return out;
+ }
+
+ function isOn(stored, id) {
+ if (IDS.indexOf(id) < 0) return true; // not a section we govern
+ return normalize(stored)[id];
+ }
+
+ function offList(stored) {
+ var s = normalize(stored);
+ return LIST.filter(function (x) { return !s[x.id]; }).map(function (x) { return x.label; });
+ }
+
+ window.WPSections = {
+ LIST: LIST,
+ IDS: IDS,
+ defaults: defaults,
+ normalize: normalize,
+ isOn: isOn,
+ offList: offList,
+ };
+})(window);
diff --git a/tests/locations_check.py b/tests/locations_check.py
index 6192436..de23338 100644
--- a/tests/locations_check.py
+++ b/tests/locations_check.py
@@ -116,8 +116,11 @@ def run(page, base, tok):
chk("...named Locations in the rail",
page.eval("(document.querySelector('#step-rail-list .step-btn[data-step=\"11\"] "
".step-btn-label')||{}).textContent") == "Locations")
- chk("...and it is the last step, so SOP complete moved with it",
- page.eval("getComputedStyle(document.getElementById('sop-complete-btn')).display")
+ # It was the last step when T5.4 shipped; T5.5 appended Sections after it. The
+ # thing that matters either way is that the wizard's own navigation follows —
+ # a step whose Next button is missing is a dead end, whichever number it has.
+ chk("...and the wizard's navigation follows it",
+ page.eval("getComputedStyle(document.getElementById('sop-next-btn')).display")
!= "none")
chk("the page boots with no JavaScript errors", not page.js_errors(), page.js_errors())
diff --git a/tests/sections_check.py b/tests/sections_check.py
new file mode 100644
index 0000000..797479e
--- /dev/null
+++ b/tests/sections_check.py
@@ -0,0 +1,451 @@
+#!/usr/bin/env python3
+"""Work package section toggles — CR-006 (T5.5).
+
+CR-006 is the structural fix behind most of the removal requests in the plan:
+rather than deleting fields globally, each project turns on only the sections it
+uses. So the check that matters most is the one about what is NOT lost — a
+toggle that quietly dropped data would be a worse version of the deletion it
+replaces.
+
+ 1. all 10 sections are toggleable
+ 2. a section that is off is absent from the form, the detail view and the
+ PDF export
+ 3. toggling off then on restores prior data with no loss
+ 4. new SOPs default to all sections on
+ 5. toggle state propagates into the creator — and where it does not, exactly
+ where and why (X4)
+
+Check 5 is the one X4 warned about. The creator is an iframe child until T7.1,
+so there are two propagation paths and they fail differently: the SOP the
+creator reads at ITS boot, and a live hand-off while the frame is already open.
+Both are exercised, separately, so a claim about one cannot cover for the other.
+
+The PDF export is checked by building the print document the way printPackage()
+does — it reuses #pkg-doc's HTML — rather than by opening a print window, which
+headless cannot show and a human cannot diff.
+
+Exit 0 all passed, 1 a failure, 2 could not run.
+"""
+import json
+import os
+import subprocess
+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, _c # noqa: E402
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+STUB = """
+window.__dialogs = [];
+window.alert = function (m) { window.__dialogs.push(['alert', String(m)]); };
+window.confirm = function (m) { window.__dialogs.push(['confirm', String(m)]); return false; };
+window.prompt = function (m) { window.__dialogs.push(['prompt', String(m)]); return null; };
+true
+"""
+
+# The ten CR-006 names to their creator card ids. Written out here rather than
+# read from wp-sections.js so the probe checks the mapping instead of agreeing
+# with it.
+SECTIONS = [
+ ("general", "General Information", "#general-card"),
+ ("location", "Location", "#location-card"),
+ ("scope", "Scope of Work", "#scope-card"),
+ ("assets", "Assets", "#asset-card"),
+ ("materials", "Materials", "#material-card"),
+ ("kitting", "Kitting", "#mimo-card"),
+ ("drawings", "Drawings and Attachments", "#drawings-card"),
+ ("constraints", "Constraints", "#constraint-card"),
+ ("qaqc", "QA/QC", "#quality-card"),
+ ("closeout", "Closeout", "#closeout-card"),
+]
+
+# A package with content in every section, so "the section is gone" and "the
+# section was empty anyway" cannot be confused.
+FULL_PKG = {
+ "id": "wpSec1", "number": "WP99-SECT", "subject": "section probe",
+ "type": "Conduit Install", "status": "Draft", "project": "Job A",
+ "location": "PROBE LOCATION VALUE", "system": "SYS-1", "cost": "", "wbs": "",
+ "assignees": "", "distribution": "", "due": "", "spec": "", "desc": "",
+ "hours": "12", "seq": "", "predecessors": [],
+ "assets": [{"tag": "PROBE-ASSET-TAG", "desc": "an asset", "link": ""}],
+ "workSteps": ["PROBE SCOPE STEP"],
+ "materials": [{"qty": "2", "unit": "ea", "desc": "PROBE MATERIAL LINE"}],
+ "attachments": [{"doc": "PROBE DRAWING DOC", "rev": "A", "link": ""}],
+ "kitStatus": "PROBE KIT STATUS", "kitOwner": "", "kitDate": "",
+ "mimoTime": "", "mimoLoc": "",
+ "constraints": [{"name": "PROBE CONSTRAINT", "status": "open", "comment": ""}],
+ "qc": "PROBE QC VALUE", "photo": "", "hold": "",
+ "signoffs": [{"role": "Superintendent", "name": "", "date": "", "signed": False}],
+ "actualHrs": "7", "installedQty": "PROBE INSTALLED QTY", "redlines": "", "lessons": "",
+}
+
+# One string per section that must vanish from the rendered document with it.
+MARKERS = {
+ "location": "PROBE LOCATION VALUE",
+ "assets": "PROBE-ASSET-TAG",
+ "scope": "PROBE SCOPE STEP",
+ "materials": "PROBE MATERIAL LINE",
+ "drawings": "PROBE DRAWING DOC",
+ "kitting": "PROBE KIT STATUS",
+ "constraints": "PROBE CONSTRAINT",
+ "qaqc": "PROBE QC VALUE",
+ "closeout": "PROBE INSTALLED QTY",
+}
+
+
+def settle(seconds=1.4):
+ 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 sop_with_sections(sections):
+ """A SOP row shaped the way ProjectData.pushSOP writes one."""
+ return {
+ "sop": {"meta": {"tool": "Work Package Configuration", "sample": False},
+ "project": {"name": "Job A", "number": "A-1", "client": "Internal QA"},
+ "governance": {"disciplines": ["Mechanical", "Electrical"],
+ "woFormat": "WP##-[TYPE]"},
+ "woTypes": [{"name": "Conduit Install", "enabled": True}],
+ "sections": sections},
+ "state": {"project": {"name": "Job A", "number": "A-1", "client": "Internal QA",
+ "division": "Internal", "site": "QA Lab"},
+ "team": {"pm": "", "apm": "", "cm": "", "qm": ""},
+ "teamIds": {"pm": "", "apm": "", "cm": "", "qm": ""},
+ "teamMembers": [], "sections": sections,
+ "signoffRoles": [{"role": "Superintendent", "name": ""},
+ {"role": "Foreman", "name": ""}],
+ "wpTypes": [{"name": "Conduit Install", "enabled": True}],
+ "governance": {"woformat": "WP##-[TYPE]", "wosize": "", "issuance": [],
+ "disciplines": ["Mechanical", "Electrical"],
+ "discMode": "choice", "instanceSuffix": "letter",
+ "sizeHoursMax": ""},
+ "quality": {"qcreq": "Yes", "photo": "", "hold": ""},
+ "platforms": {"tracking": "CxAlloy", "commissioning": "CxAlloy",
+ "trackingUrl": "", "commissioningUrl": ""},
+ "constraints": [], "sequence": [], "sources": []},
+ }
+
+
+def set_sop(db_path, sections):
+ from server.db import SessionLocal
+ from server import models
+ with SessionLocal() as db:
+ sop = db.get(models.Sop, "sopA")
+ sop.data = sop_with_sections(sections)
+ db.commit()
+
+
+def render_full(page):
+ """Load the probe package into the creator and render it as a document."""
+ page.eval("window.__probePkg = %s; true" % json.dumps(FULL_PKG))
+ page.eval("renderPackage(window.__probePkg)")
+ settle(0.7)
+ return page.eval("document.getElementById('pkg-doc').innerHTML")
+
+
+def print_doc(page):
+ """What printPackage() would put in the print window. It reuses #pkg-doc's
+ HTML verbatim, so this is the export, not an approximation of it."""
+ return page.eval("document.getElementById('pkg-doc').innerHTML")
+
+
+def run(page, base, tok, db_path):
+ print("\n4. a new SOP defaults to every section on")
+ page.clear_cookies()
+ page.set_cookie("wp_session", tok["root"])
+ page.goto(base + "/work-package-suite.html?project=projA&step=12")
+ settle(2.0)
+ page.eval(STUB)
+ chk("the page boots with no JavaScript errors", not page.js_errors(), page.js_errors())
+ rows = json.loads(page.eval("""JSON.stringify(
+ [...document.querySelectorAll('#section-toggles .section-toggle')].map(l => ({
+ id: (l.querySelector('input')||{}).dataset.section,
+ label: ((l.querySelector('.section-toggle-name')||{}).textContent||'').trim(),
+ note: ((l.querySelector('.section-toggle-note')||{}).textContent||'').trim(),
+ on: !!(l.querySelector('input')||{}).checked,
+ state: ((l.querySelector('.section-toggle-state')||{}).textContent||'').trim(),
+ tag: (l.querySelector('input')||{}).type,
+ })))"""))
+ chk("all ten sections are listed", len(rows) == 10, [r["id"] for r in rows])
+ chk("...with the ids CR-006 names",
+ [r["id"] for r in rows] == [s[0] for s in SECTIONS], [r["id"] for r in rows])
+ chk("...and the labels CR-006 names",
+ [r["label"] for r in rows] == [s[1] for s in SECTIONS], [r["label"] for r in rows])
+ chk("...every one a real checkbox, not a div with a handler",
+ all(r["tag"] == "checkbox" for r in rows), [r["tag"] for r in rows])
+ chk("...every one explaining what it governs",
+ all(len(r["note"]) > 20 for r in rows), [r["note"][:30] for r in rows])
+ chk("a new SOP has all ten ON", all(r["on"] for r in rows), [r["id"] for r in rows if not r["on"]])
+ chk("...and says so in words, not only by the tick",
+ all(r["state"] == "In use" for r in rows), [r["state"] for r in rows])
+ chk("the summary reports nothing turned off",
+ "Every section is in use" in (page.eval(
+ "(document.getElementById('section-summary')||{}).textContent||''")),
+ page.eval("(document.getElementById('section-summary')||{}).textContent||''"))
+
+ print("\n1. every one of the ten can be turned off and back on")
+ for sec_id, label, _sel in SECTIONS:
+ ok = page.eval("""(() => {
+ const b = document.querySelector('#section-toggles input[data-section=%s]');
+ if (!b) return 'missing';
+ b.checked = false; b.dispatchEvent(new Event('change', {bubbles:true}));
+ const off = !b.checked;
+ b.checked = true; b.dispatchEvent(new Event('change', {bubbles:true}));
+ return (off && b.checked) ? 'ok' : 'stuck';
+ })()""" % json.dumps(sec_id))
+ chk("%-12s toggles off and on" % sec_id, ok == "ok", ok)
+ summary_off = page.eval("""(() => {
+ const b = document.querySelector('#section-toggles input[data-section="assets"]');
+ b.checked = false; b.dispatchEvent(new Event('change', {bubbles:true}));
+ return (document.getElementById('section-summary')||{}).textContent||'';
+ })()""")
+ chk("the summary names what was turned off", "Assets" in summary_off, repr(summary_off))
+ chk("...and says the data is kept", "retained" in summary_off, repr(summary_off))
+
+ print("\n5a. the creator honours the SOP it boots with (X4, path one)")
+ off = {"assets": False, "kitting": False}
+ set_sop(db_path, off)
+ page.goto(base + "/wp-creation-index.html?project=projA")
+ chk("the creator boots", wait_creator(page))
+ settle(1.4)
+ page.eval(STUB)
+ vis = json.loads(page.eval("""JSON.stringify(%s.map(sel => {
+ const el = document.querySelector(sel);
+ return [sel, !el ? 'missing' : (el.hidden ? 'hidden' : 'shown')];
+ }))""" % json.dumps([s[2] for s in SECTIONS])))
+ by_sel = dict(vis)
+ chk("Assets is absent from the form", by_sel["#asset-card"] == "hidden", vis)
+ chk("Kitting is absent from the form", by_sel["#mimo-card"] == "hidden", vis)
+ chk("...and every other section is still there",
+ all(v == "shown" for k, v in by_sel.items() if k not in ("#asset-card", "#mimo-card")),
+ vis)
+ chips = page.eval("(document.getElementById('section-nav')||{}).textContent||''")
+ chk("the section chip strip loses the same entries",
+ "Assets" not in chips and "Kitting" not in chips, repr(chips))
+ chk("...and keeps the rest", "Scope" in chips and "Constraints" in chips, repr(chips))
+
+ print("\n2. and from the detail view and the PDF export")
+ doc = render_full(page)
+ chk("the package renders", "WP99-SECT" in doc, doc[:120])
+ chk("Assets content is absent from the document", MARKERS["assets"] not in doc)
+ chk("Kitting content is absent from the document", MARKERS["kitting"] not in doc)
+ chk("...while every other section's content is present",
+ all(v in doc for k, v in MARKERS.items() if k not in ("assets", "kitting")),
+ [k for k, v in MARKERS.items() if v not in doc])
+ chk("no empty heading is left where a section was",
+ "Assets" not in doc and "Kitting" not in doc, [ln for ln in doc.split("
")[1:4]])
+ heads = [h.split(" ")[0] for h in doc.split("")[1:]]
+ chk("...and the surviving sections are renumbered without gaps",
+ [h.split(".0")[0] for h in heads] == [str(i + 1) for i in range(len(heads))], heads)
+ export = print_doc(page)
+ chk("the PDF export is the same document, so it lost them too",
+ export == doc and MARKERS["assets"] not in export)
+
+ print("\n3. turning a section back on restores its data intact")
+ set_sop(db_path, {})
+ page.goto(base + "/wp-creation-index.html?project=projA")
+ chk("the creator boots again", wait_creator(page))
+ settle(1.4)
+ page.eval(STUB)
+ back = json.loads(page.eval("""JSON.stringify(%s.map(sel => {
+ const el = document.querySelector(sel);
+ return [sel, !el ? 'missing' : (el.hidden ? 'hidden' : 'shown')];
+ }))""" % json.dumps([s[2] for s in SECTIONS])))
+ chk("every section is back in the form", all(v == "shown" for _k, v in back), back)
+ doc2 = render_full(page)
+ chk("...and every marker is back in the document",
+ all(v in doc2 for v in MARKERS.values()),
+ [k for k, v in MARKERS.items() if v not in doc2])
+ chk("...including the two that were suppressed",
+ MARKERS["assets"] in doc2 and MARKERS["kitting"] in doc2)
+ # The real question is not whether the probe's own object survived — nothing
+ # was ever going to touch that. It is whether a package EDITED while a section
+ # is off keeps that section's content when it is saved. So: turn Assets off,
+ # load a package that has assets into the form, and collect it the way Save
+ # does. If hiding a card emptied what it renders, this is where it shows.
+ set_sop(db_path, {"assets": False})
+ page.goto(base + "/wp-creation-index.html?project=projA")
+ chk("the creator boots with Assets off", wait_creator(page))
+ settle(1.2)
+ page.eval(STUB)
+ collected = json.loads(page.eval("""(() => {
+ loadPackageIntoForm(%s);
+ const out = collectPackage();
+ return JSON.stringify({assets: out.assets, materials: out.materials,
+ kitStatus: out.kitStatus, subject: out.subject});
+ })()""" % json.dumps(FULL_PKG)))
+ chk("a package edited while Assets is off keeps its assets on save",
+ collected["assets"] == FULL_PKG["assets"], collected["assets"])
+ # Compared on the content, not the whole row: the creator upper-cases a
+ # material unit on its way through the form ("ea" -> "EA"), which is its own
+ # long-standing behaviour and nothing to do with section toggles. Asserting
+ # byte-equality here would fail on that and read as data loss.
+ chk("...and the sections that were on are untouched too",
+ [m["desc"] for m in collected["materials"]]
+ == [m["desc"] for m in FULL_PKG["materials"]]
+ and collected["subject"] == FULL_PKG["subject"], collected)
+ set_sop(db_path, {})
+
+ print("\n5b. a toggle flipped while the frame is open reaches it live (X4, path two)")
+ page.goto(base + "/work-package-suite.html?project=projA&tab=wp")
+ for _ in range(40):
+ inner = page.eval("""(() => {
+ try { const f = document.getElementById('wp-frame');
+ return !!(f && f.contentWindow && f.contentWindow.wpCreatorReady); }
+ catch (e) { return false; }
+ })()""")
+ if inner:
+ break
+ time.sleep(0.3)
+ settle(1.6)
+ page.eval(STUB)
+ chk("the creator is loaded in the frame", page.eval("""(() => {
+ try { return !!document.getElementById('wp-frame').contentWindow.wpCreatorReady; }
+ catch (e) { return false; }
+ })()"""))
+ chk("...showing Assets to begin with", page.eval("""(() => {
+ const d = document.getElementById('wp-frame').contentDocument;
+ const el = d.querySelector('#asset-card');
+ return !!el && !el.hidden;
+ })()"""))
+ page.eval("goToStep(12)")
+ settle(0.8)
+ page.eval("""(() => {
+ const b = document.querySelector('#section-toggles input[data-section="assets"]');
+ b.checked = false; b.dispatchEvent(new Event('change', {bubbles:true}));
+ return true;
+ })()""")
+ settle(0.8)
+ chk("turning Assets off reaches the already-loaded frame, with no reload",
+ page.eval("""(() => {
+ const d = document.getElementById('wp-frame').contentDocument;
+ const el = d.querySelector('#asset-card');
+ return !!el && el.hidden;
+ })()"""))
+ page.eval("""(() => {
+ const b = document.querySelector('#section-toggles input[data-section="assets"]');
+ b.checked = true; b.dispatchEvent(new Event('change', {bubbles:true}));
+ return true;
+ })()""")
+ settle(0.8)
+ chk("...and turning it back on brings it back",
+ page.eval("""(() => {
+ const d = document.getElementById('wp-frame').contentDocument;
+ const el = d.querySelector('#asset-card');
+ return !!el && !el.hidden;
+ })()"""))
+
+ print("\n an older SOP that predates CR-006 gets every section, not none")
+ set_sop_no_sections(db_path)
+ page.goto(base + "/wp-creation-index.html?project=projA")
+ chk("the creator boots on a SOP with no sections key", wait_creator(page))
+ settle(1.2)
+ legacy = json.loads(page.eval("""JSON.stringify(%s.map(sel => {
+ const el = document.querySelector(sel);
+ return [sel, !el ? 'missing' : (el.hidden ? 'hidden' : 'shown')];
+ }))""" % json.dumps([s[2] for s in SECTIONS])))
+ chk("...and shows all ten, because silence means on", all(v == "shown" for _k, v in legacy),
+ legacy)
+
+ print("\n the registry is shared, not copied")
+ for name in ("work-package-suite.html", "wp-creation-index.html"):
+ src = open(os.path.join(ROOT, "html", name), encoding="utf-8").read()
+ chk("%-26s loads wp-sections.js" % name, "wp-sections.js" in src)
+ shared = open(os.path.join(ROOT, "html", "wp-sections.js"), encoding="utf-8").read()
+ for name in ("work-package-suite-app.js", "wp-creation-app.js"):
+ src = open(os.path.join(ROOT, "html", name), encoding="utf-8").read()
+ chk("%-26s declares no second section list" % name,
+ "'Drawings and Attachments'" not in src and '"Drawings and Attachments"' not in src)
+ chk("the shared list is the only place the labels live",
+ shared.count("Drawings and Attachments") == 1)
+
+ chk("no native dialog was opened anywhere in this flow",
+ not json.loads(page.eval("JSON.stringify(window.__dialogs||[])")),
+ page.eval("JSON.stringify(window.__dialogs||[])"))
+
+
+def set_sop_no_sections(db_path):
+ from server.db import SessionLocal
+ from server import models
+ with SessionLocal() as db:
+ sop = db.get(models.Sop, "sopA")
+ data = dict(sop.data or {})
+ inner = dict(data.get("sop") or {})
+ inner.pop("sections", None)
+ state = dict(data.get("state") or {})
+ state.pop("sections", None)
+ sop.data = {"sop": inner, "state": state}
+ db.commit()
+
+
+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-sections-")
+ db_path = os.path.join(tmpdir, "check.db")
+ server = 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)
+ if server is None:
+ print("the test server would not start.")
+ return 2
+ print("\nSection toggles — CR-006\nTarget: %s" % base)
+
+ browser = cdp.Browser(exe)
+ page = browser.page()
+ try:
+ run(page, base, tok, db_path)
+ finally:
+ page.close()
+ browser.close()
+ finally:
+ if server:
+ server.kill()
+ try:
+ server.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ pass
+ try:
+ from server.db import engine
+ engine.dispose()
+ except Exception:
+ pass
+ import shutil
+ for _ in range(10):
+ shutil.rmtree(tmpdir, ignore_errors=True)
+ if not os.path.exists(tmpdir):
+ break
+ time.sleep(0.3)
+
+ total = len(_PASS) + len(_FAIL)
+ print("\n%s\n%d/%d checks passed." % ("-" * 54, len(_PASS), total))
+ if _FAIL:
+ for f in _FAIL:
+ print(" - " + f)
+ return 1
+ print("\nResult: " + _c("ALL PASS — off means hidden, never deleted.", "32") + "\n")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tests/stepper_check.py b/tests/stepper_check.py
index 63578f3..1f0cca8 100644
--- a/tests/stepper_check.py
+++ b/tests/stepper_check.py
@@ -51,9 +51,10 @@ HTML_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__
# Wave 0 recorded 12. T5.1 must take ten of them.
BASELINE_DIV_ONCLICK = 12
-# Ten until T5.4 (CR-005) appended Locations. Named rather than repeated, so the
-# next step to be added moves one number instead of eight assertions.
-STEP_COUNT = 11
+# Ten at T5.1; +1 at T5.4 (CR-005 Locations); +1 at T5.5 (CR-006 Sections). Named
+# rather than repeated, so the next step to be added moves one number instead of
+# eight assertions.
+STEP_COUNT = 12
# Swallow the wizard's remaining native dialogs and record that they fired. The
# wizard still has them until T5.8; this proves the RAIL never reaches one.