#!/usr/bin/env python3 """Does the creator's form have structure? — F6 / D3, T7.2. `F6` measured the creator at 11 cards in one 5,399px scroll with a strip of jump chips standing in for structure. `D3` settled the shape: not tabs — one page, a rail down the side, sections collapsible, only the current one open, plus an `Expand all` for people who would rather scroll straight through. It also amended `F6`'s height criterion to **at rest**, in writing, because the answer chosen and the criterion as written could not both hold. 1. F6 no longer reproduces: at rest the page is under two screen heights 2. the rail is built from the cards, so CR-006 suppression reaches it 3. every heading is a real disclosure button, every rail entry a real button 4. a section is addressable, survives refresh, and Back moves between sections 5. unsaved work survives moving between sections (T4.3) 6. Expand all does what it says, and is remembered 7. keyboard: reach the rail by Tab, activate by Enter and by Space 8. 390px and 1440px Check 7 dispatches real key events through CDP. `page.key()` dispatches a synthetic KeyboardEvent on `document`, which never reaches a listener bound to a button and never triggers native activation — a rail with no keyboard support at all would report a clean pass. That mistake was made once already, in wave 5. 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, _c # noqa: E402 from sections_check import set_sop # noqa: E402 def settle(seconds=1.0): 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 # Reused, not rewritten. stepper_check.py already carries the recipe that works - # nativeVirtualKeyCode alongside windowsVirtualKeyCode, rawKeyDown for keys with no # text, unmodifiedText for the ones that have it - and the note explaining why the # NEXT navigation after a real key press can stall the whole CDP session. from stepper_check import press, dismiss_dialogs # noqa: E402 def open_creator(page, base, tok, query="?project=projA", width=1440): page.clear_cookies() page.set_cookie("wp_session", tok["root"]) page.viewport(width, 900, mobile=(width <= 500)) # BL-020, and it bites the probe before it bites anybody else: once a real key # press has given the page sticky user activation, WPAutosave's beforeunload # guard can raise a browser-level "Leave site?" prompt on the next navigation. # It is not JavaScript, so the alert() stub cannot see it, and an unanswered one # stalls the CDP session rather than failing a check. Clear the form's dirty # state first, then answer anything that still opens. if page.eval("!!window.wpCreatorReady") is True: # Only meaningful once a creator is actually loaded. Running it against # about:blank on the first call cost this probe its very first check. page.eval("typeof wpMarkFormClean === 'function' ? (wpMarkFormClean(), 1) : 0") page.goto(base + "/wp-creation-index.html" + query) dismiss_dialogs(page) ok = wait_creator(page) settle(1.6) return ok RAIL_JS = """(() => JSON.stringify( [...document.querySelectorAll('.sec-rail-item')].map(b => ({ sec: b.dataset.sec, label: (b.textContent || '').trim(), tag: b.tagName, current: b.getAttribute('aria-current') === 'true', }))))()""" CARDS_JS = """(() => JSON.stringify( [...document.querySelectorAll('.main > .card')] .filter(c => c.id !== 'saved-card' && !c.hidden && c.style.display !== 'none' && c.querySelector('.section-title, .sub-heading')) .map(c => { const btn = c.querySelector('.card-toggle'); const body = c.querySelector(':scope > .card-body'); return { id: c.id, toggleTag: btn ? btn.tagName : null, expanded: btn ? btn.getAttribute('aria-expanded') : null, controls: btn ? btn.getAttribute('aria-controls') : null, controlsExists: btn && btn.getAttribute('aria-controls') ? !!document.getElementById(btn.getAttribute('aria-controls')) : false, bodyHidden: body ? !!body.hidden : null, collapsed: c.classList.contains('collapsed'), }; })))()""" 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-formstruct-") db_path = os.path.join(tmpdir, "check.db") server = None try: tok = seed(db_path) set_sop(db_path, {}) # a SOP the app can read (BL-018) 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("\nThe creator's form structure - F6 / D3\nTarget: %s" % base) browser = cdp.Browser(exe) page = browser.page() try: run(page, base, tok, db_path) finally: page.close() browser.close() print("\n" + "-" * 54) total = len(_PASS) + len(_FAIL) print("%d/%d checks passed." % (len(_PASS), total)) for f in _FAIL: print(" - " + f) if _FAIL: return 1 print("\nResult: " + _c("ALL PASS", "32") + " - the form has structure.") return 0 finally: if server is not None: try: server.terminate() except Exception: pass def run(page, base, tok, db_path): # ── 1. F6 ──────────────────────────────────────────────────────────────── print("\n1. F6: the 5,399px scroll") chk("the creator boots", open_creator(page, base, tok)) h = json.loads(page.eval("""JSON.stringify({ scroll: document.documentElement.scrollHeight, view: window.innerHeight, cards: document.querySelectorAll('.main > .card').length, chips: document.querySelectorAll('.sec-chip').length, bar: document.querySelectorAll('.section-nav-bar').length, })""")) screens = h["scroll"] / float(h["view"] or 1) print(" at rest: %spx over %spx viewport = %.2f screens, %d cards" % (h["scroll"], h["view"], screens, h["cards"])) # D3's amendment, quoted: "no single view exceeds roughly two screen heights at # 1440px AT REST - that is, with the default collapse state, which is the state # the page is actually in when it loads." chk("at rest the page is under two screen heights (F6 / D3)", screens <= 2.0, "%.2f screens (%spx / %spx)" % (screens, h["scroll"], h["view"])) chk("the jump chips are gone", h["chips"] == 0, h["chips"]) chk("...and so is the strip that held them", h["bar"] == 0, h["bar"]) # Comments stripped first. BL-017 is the entry about a metric that counted its # own explanation, and the comment above this rewrite contains the words # "" precisely because it is about removing them. src = open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "html", "wp-creation-app.js"), encoding="utf-8").read() code = re.sub(r"/\*.*?\*/", "", src, flags=re.S) code = re.sub(r"(?m)^\s*//.*$", "", code) chk("the creator builds no section chip at all", "sec-chip" not in code, [l for l in code.splitlines() if "sec-chip" in l][:2]) # The dashboard's status filter is the OTHER . It is not this # task's - the C1 audit at T9.5 drives the app-wide count to 0 - but counting it # here means T7.2 cannot be read as having cleared something it did not. spans = len(re.findall(r"]*onclick", code)) print(" still built by the creator: %d (dashboard status chip; T9.5)" % spans) # T9.5 converted the dashboard chip to a button, so the count is 0 now - # pinned there, because a new span-with-onclick would be a C1 regression. chk("...and none is left in this file at all (the chip became a button at T9.5)", spans == 0, spans) # ── 2. the rail ────────────────────────────────────────────────────────── print("\n2. the rail is built from the cards") rail = json.loads(page.eval(RAIL_JS)) cards = json.loads(page.eval(CARDS_JS)) chk("the rail has an entry per visible section", len(rail) == len(cards), "%d entries vs %d sections" % (len(rail), len(cards))) chk("...in the same order as the form", [r["sec"] for r in rail] == [c["id"] for c in cards], {"rail": [r["sec"] for r in rail], "form": [c["id"] for c in cards]}) chk("...every one of them a real button", all(r["tag"] == "BUTTON" for r in rail), [r["tag"] for r in rail]) chk("...and every entry is named", all(r["label"] for r in rail), rail) chk("exactly one is marked current", sum(1 for r in rail if r["current"]) == 1, [r["label"] for r in rail if r["current"]]) print("\n CR-006: a suppressed section leaves the rail with the form") labels_before = [r["label"] for r in rail] set_sop(db_path, {"assets": False}) open_creator(page, base, tok) rail2 = json.loads(page.eval(RAIL_JS)) labels_after = [r["label"] for r in rail2] chk("Assets is gone from the form", page.eval("""(() => { const el = document.querySelector('#asset-card'); return !!el && el.hidden; })()""")) chk("...and gone from the rail", "Assets" not in labels_after, labels_after) chk("...and nothing else left with it", [l for l in labels_before if l != "Assets"] == labels_after, {"before": labels_before, "after": labels_after}) set_sop(db_path, {}) # ── 3. real controls ───────────────────────────────────────────────────── print("\n3. every heading is a real disclosure button") open_creator(page, base, tok) cards = json.loads(page.eval(CARDS_JS)) chk("every section has a