#!/usr/bin/env python3 """Does the constraint warning appear once, and can you still tell? — A2, T7.4. `A2`: the same not-release-ready warning rendered three times — the top banner, the sticky save bar, and a static hint under the status control. The banner stays; the count moves to a badge on the Constraints rail entry, which is sticky at every width, so it is visible from any section without repeating the sentence anywhere. The sticky bar's copy was worse than noise: it was written with textContent into the SAME span the B5 autosave indicator mounts into, so every count change destroyed the indicator. Check 4 pins the survival. Self-contained: throwaway SQLite, its own uvicorn, headless Edge or Chrome. 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 def ascii_(v, n=300): return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n] def settle(seconds=0.6): 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 constraint_btn(page, index, which): labels = {"open": "Open", "cleared": "Cleared", "na": "N/A"} page.eval("""(() => { const tr = document.querySelectorAll('#constraint-body tr')[%d]; [...tr.querySelectorAll('.cstatus button')] .find(b => b.textContent.trim() === %s).click(); })()""" % (index, json.dumps(labels[which]))) settle(0.4) # Every element whose OWN text says "not release-ready" (or the on-hold variant), # deduplicated to the outermost matches. This is the A2 count. WARN_COUNT_JS = """(() => { const rx = /Not release-ready|On hold —/; const hits = [...document.querySelectorAll('body *')].filter(el => rx.test(el.textContent || '') && ![...el.children].some(ch => rx.test(ch.textContent || ''))); return JSON.stringify(hits.map(el => el.tagName.toLowerCase() + (el.id ? '#'+el.id : '') + (el.className && typeof el.className === 'string' ? '.'+el.className.trim().split(/\\s+/)[0] : ''))); })()""" def warn_sites(page): return json.loads(page.eval(WARN_COUNT_JS)) 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-warn-") 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) browser = cdp.Browser(exe) page = browser.page() page.clear_cookies() page.set_cookie("wp_session", tok["root"]) page.viewport(1440, 900) page.goto(base + "/wp-creation-index.html?project=projA") dismiss_dialogs(page) chk("the creator boots", wait_creator(page)) settle(1.5) # Native dialogs block Runtime.evaluate outright - the release-ready offer # fires when the last open constraint changes, so stub before touching one. page.eval("""(() => { window.prompt = () => null; window.alert = () => {}; window.confirm = () => false; })()""") # ── 1. one warning ──────────────────────────────────────────────────── print("\n1. the warning appears once") sites = warn_sites(page) chk("with constraints open, the not-release-ready warning renders exactly once", len(sites) == 1, ascii_(sites)) chk("...and it is the top banner", len(sites) == 1 and sites[0].startswith("div.rb-inner"), ascii_(sites)) chk("the static hint under the status control is gone", page.eval("""(() => ![...document.querySelectorAll('.field-hint')] .some(h => /until all constraints are cleared/.test(h.textContent)))()""")) chk("the sticky save bar carries no readiness text", page.eval("""(() => !/release-ready|constraint/i.test( (document.getElementById('sticky-status')||{textContent:''}).textContent))()""")) # ── 2. the badge ───────────────────────────────────────────────────── print("\n2. the badge on the Constraints rail entry") open_now = page.eval("readiness().open") badge = lambda: json.loads(page.eval("""JSON.stringify((() => { const b = document.querySelector( '.sec-rail-item[data-sec="constraint-card"] .sec-badge'); if (!b) return null; const cs = getComputedStyle(b); const r = b.getBoundingClientRect(); return {text: b.textContent, hidden: b.hidden, display: cs.display, size: parseFloat(cs.fontSize), w: r.width, h: r.height, top: r.top, bottom: r.bottom}; })())""")) b = badge() chk("the badge exists on the rail entry and shows the open count", b is not None and b["text"] == str(open_now) and not b["hidden"], ascii_(b)) chk("...and the entry SAYS it for a screen reader", "open" in (page.eval("""(document.querySelector( '.sec-rail-item[data-sec="constraint-card"]')||{getAttribute:()=>''}) .getAttribute('aria-label')""") or "")) constraint_btn(page, 0, "cleared") b2 = badge() chk("clearing a constraint moves the count", b2 and b2["text"] == str(open_now - 1), ascii_(b2)) n = page.eval("pkgConstraints.length") for i in range(n): constraint_btn(page, i, "na") dismiss_dialogs(page) b3 = badge() chk("at zero open the badge disappears instead of showing a calm-looking 0", b3 is None or b3["hidden"] or b3["display"] == "none", ascii_(b3)) constraint_btn(page, 0, "open") # ── 3. visible from any section, badge legible, both widths ────────── print("\n3. out of sight is not out of mind") page.eval("gotoSection('signoff-card')") settle(0.8) page.eval("window.scrollTo(0, document.documentElement.scrollHeight)") settle(0.6) # "Out of view" means the INFORMATION is out of view: the page is barely # two screens at rest, so the section's collapsed HEADER is almost always # somewhere on screen - but a collapsed header says nothing about open # counts. What must be simultaneously true: the constraint table is not # visible, the banner is not visible, and the badge is. info_off = page.eval("""(() => { const t = document.getElementById('constraint-body'); const tr = t ? t.getBoundingClientRect() : null; const tableGone = !t || t.offsetParent === null || tr.height === 0 || tr.bottom < 0 || tr.top > innerHeight; const w = document.querySelector('#release-banner .rb-inner'); const wr = w ? w.getBoundingClientRect() : null; const bannerGone = !w || wr.bottom < 0 || wr.top > innerHeight; return tableGone && bannerGone; })()""") b4 = badge() vis = b4 and not b4["hidden"] and b4["top"] >= 0 and b4["bottom"] <= 900 chk("1440px: table and banner both out of view, the badge still on screen", info_off and bool(vis), ascii_((info_off, b4))) page.viewport(390, 900, mobile=True) settle(0.8) page.eval("window.scrollTo(0, document.documentElement.scrollHeight)") settle(0.6) b5 = badge() chk("390px: the badge renders at a legible size (>= 12px text, >= 16px tall)", b5 and not b5["hidden"] and b5["size"] >= 12 and b5["h"] >= 16, ascii_(b5)) chk("390px: scrolled to the bottom, the badge is still on screen (sticky rail)", b5 and b5["top"] >= 0 and b5["bottom"] <= 900, ascii_(b5)) page.viewport(1440, 900) settle(0.6) # ── 4. the live region, and the indicator it used to destroy ───────── print("\n4. announcements") chk("the banner is a polite live region (role=status, the login.html pattern)", page.eval("(document.getElementById('release-banner')||{}).getAttribute" "&&document.getElementById('release-banner').getAttribute('role')") == "status") page.eval("document.querySelector('#release-banner .rb-inner').dataset.probe='x'") page.eval("updateReleaseBanner()") settle(0.3) chk("a no-op refresh does NOT rewrite the region (no phantom announcements)", page.eval("(document.querySelector('#release-banner .rb-inner')||{dataset:{}})" ".dataset.probe") == "x") before = page.eval("document.querySelector('#release-banner .rb-inner').textContent") constraint_btn(page, 1, "open") after = page.eval("document.querySelector('#release-banner .rb-inner').textContent") chk("a count change rewrites the region, which is what announces it", before != after and page.eval( "(document.querySelector('#release-banner .rb-inner')||{dataset:{}})" ".dataset.probe") != "x", ascii_((before, after))) chk("the B5 autosave indicator survived every one of those updates", page.eval("!!document.querySelector('#sticky-status #wp-draft-status')")) js_errors = [e for e in page.js_errors()] 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())