#!/usr/bin/env python3 """Is the creator's iframe actually gone? — B7 / T7.1, plus D1. `docs/reference/creator-frame.md` measured the boundary before it was dissolved: 21 colliding stylesheet selectors, 9 colliding script globals, 0 colliding markup ids, 28 cross-frame call sites across 8 scripts and 1 page. This checks the other end of that work. 1. no iframe, and no cross-frame code, anywhere in html/ 2. both documents still parse and boot - the failure mode a structural edit has 3. the creator is a page: app bar, tab strip, drawer, sample controls (D1) 4. every address that used to reach the creator through the frame still reaches it 5. the SOP gate still gates, and a gated tab explains itself instead of vanishing 6. every creator feature the frame used to hide is reachable 7. the four backlog entries logged against this file, re-measured Check 2 is here because of a real failure during T7.1: a `const` shadowing a function parameter is a SyntaxError, so `work-package-suite-app.js` did not parse at all and every one of its globals was undefined. Four unrelated checks in another probe went red and none of them said "the script did not load". A probe that asserts a page's own entry points exist is cheap and says exactly that. Self-contained: throwaway SQLite, its own uvicorn, headless Edge or Chrome. Exit 0 all passed, 1 a failure, 2 could not run. """ import io 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 # BL-018, for the fourth time: browser_check.seed() writes a SOP whose data is # {"governance": ...}, and nothing in the app writes that shape. restoreSavedSOP() # bails on it, sopComplete stays false, and every creator route below would land on # the SOP gate instead. Imported rather than copied, so the duplication stays # visible in one place until T9.9 fixes the fixture itself. from sections_check import set_sop # noqa: E402 HTML = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "html") # Which boxes are wider than the screen, worst first. BL-001's own entry blamed # --nav-w; naming the actual elements means the next person opens the right file # instead of re-deriving it. WIDEST_JS = r"""(() => { const vw = document.documentElement.clientWidth; const sw = document.documentElement.scrollWidth; const out = []; document.querySelectorAll('body *').forEach(el => { const cs = getComputedStyle(el); // A `position: fixed` drawer parked off-screen with translateX(100%) sits far // to the right of the viewport by design and contributes nothing to // scrollWidth. Its CHILDREN are `position: static` and ride out there with it, // so testing the element alone is not enough - the whole subtree has to go, or // the list fills with the five boxes inside the drawer and BL-001 gets // attributed to the wrong file for a second time. for (let p = el; p; p = p.parentElement) { if (getComputedStyle(p).position === 'fixed') return; } const r = el.getBoundingClientRect(); if (r.right > vw + 2 || r.width > vw + 2) { out.push({ sel: el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') + (el.className && typeof el.className === 'string' ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : ''), w: Math.round(r.width), right: Math.round(r.right), pos: cs.position, }); } }); out.sort((a, b) => b.right - a.right); return JSON.stringify({scrollWidth: sw, viewport: vw, boxes: out.slice(0, 5)}); })()""" 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 def read(name): return io.open(os.path.join(HTML, name), encoding="utf-8").read() def strip_comments(src, kind): """Comments are prose about the change, and prose about removing an iframe contains the word iframe. BL-017 recorded exactly this: a grep that counts its own explanation moves the number it is meant to be proving. Strip them. """ if kind == "html": return re.sub(r"", "", src, flags=re.S) src = re.sub(r"/\*.*?\*/", "", src, flags=re.S) return re.sub(r"(?m)^\s*//.*$", "", src) # ── 1. the source ───────────────────────────────────────────────────────────── def source_checks(): print("\n1. no iframe, and no cross-frame code, in html/") pages = [f for f in os.listdir(HTML) if f.endswith(".html")] scripts = [f for f in os.listdir(HTML) if f.endswith(".js")] framed = [] for f in pages: body = strip_comments(read(f), "html") if " { const cur = document.querySelector('.nav-tab[aria-current="page"]'); return !!cur && cur.getAttribute('data-tab') === 'wp'; })()"""), page.eval("""(() => { const c = document.querySelector('.nav-tab[aria-current="page"]'); return c ? c.getAttribute('data-tab') : '(none)'; })()""")) chk("...and the SOP tab is a link back to the wizard, carrying the project", page.eval("""(() => { const a = document.querySelector('a.nav-tab'); const h = a ? (a.getAttribute('href') || '') : ''; return h.indexOf('work-package-suite.html') === 0 && h.indexOf('project=projA') > 0; })()"""), page.eval("(document.querySelector('a.nav-tab')||{}).getAttribute" " ? document.querySelector('a.nav-tab').getAttribute('href') : '(none)'")) # Switching to the board must re-mark the strip; a tab row that lies about # where you are is worse than none. page.eval("showDashboard()") settle(1.2) chk("opening the board moves the current marker", page.eval("""(() => { const cur = document.querySelector('.nav-tab[aria-current="page"]'); return !!cur && cur.getAttribute('data-tab') === 'dashboard'; })()""")) page.eval("showForm()") settle(1.0) chk("...and going back to the form moves it back", page.eval("""(() => { const cur = document.querySelector('.nav-tab[aria-current="page"]'); return !!cur && cur.getAttribute('data-tab') === 'wp'; })()""")) print("\n D1: the controls body.embedded used to hide") vis = json.loads(page.eval("""(() => { const out = {}; [...document.querySelectorAll('#wp-toolbar button')].forEach(b => { const r = b.getBoundingClientRect(); out[(b.textContent || '').trim()] = r.width > 0 && r.height > 0; }); return JSON.stringify(out); })()""")) # "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"): 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) chk("...and every one of them has a handler that exists", page.eval("""(() => { return [...document.querySelectorAll('#wp-toolbar button')].every(b => { const m = (b.getAttribute('onclick') || '').match(/^([A-Za-z_$][\\w$]*)\\(/); return !m || typeof window[m[1]] === 'function'; }); })()"""), page.eval("""(() => { return JSON.stringify([...document.querySelectorAll('#wp-toolbar button')] .map(b => (b.getAttribute('onclick') || '')) .filter(o => { const m = o.match(/^([A-Za-z_$][\\w$]*)\\(/); return m && typeof window[m[1]] !== 'function'; })); })()""")) # ── 4. old addresses ────────────────────────────────────────────────────────── def address_checks(page, base, tok): print("\n4. every address that reached the creator through the frame still reaches it") # These are in bookmarks, in wp-sidenav's link map, and they are the shape the # CR-011 and CR-014 emails were specified against (X1). Breaking them silently # is the one regression this task could ship that nobody would notice for weeks. for label, path, want in ( ("?tab=wp opens the creator", "/work-package-suite.html?project=projA&tab=wp", None), ("?view=dashboard opens the board", "/work-package-suite.html?project=projA&view=dashboard", "Dashboard"), ("?wp= opens that package", "/work-package-suite.html?project=projA&wp=wpA1", "wpA1"), ): page.clear_cookies() page.set_cookie("wp_session", tok["root"]) page.goto(base + path) settle(0.8) landed = wait_creator(page) chk(label, landed and "wp-creation-index.html" in page.eval("location.pathname"), page.eval("location.pathname + location.search")) settle(1.2) if want == "Dashboard": chk("...on the board, not the form", page.eval("""(() => { const dv = document.getElementById('dashboard-view'); return !!dv && getComputedStyle(dv).display !== 'none'; })()""")) elif want == "wpA1": chk("...with that package in the form", "horn/strobe" in (page.eval( "(document.getElementById('wp_subject')||{}).value||''") or ""), page.eval("(document.getElementById('wp_subject')||{}).value||''")) print("\n ...and the forward is a replace, so Back is not a bounce") page.clear_cookies() page.set_cookie("wp_session", tok["root"]) page.goto(base + "/index.html?project=projA") settle(1.4) page.eval("location.assign('work-package-suite.html?project=projA&tab=wp')") settle(0.8) wait_creator(page) settle(1.2) page.eval("history.back()") settle(1.8) where = page.eval("location.pathname") chk("Back from a forwarded link returns to where you came from", "index.html" in where, where) # ── 5. the gate ─────────────────────────────────────────────────────────────── def gate_checks(page, base, tok): print("\n5. the SOP gate still gates, and says so") # projB has members but no SOP. page.clear_cookies() page.set_cookie("wp_session", tok["root"]) page.goto(base + "/work-package-suite.html?project=projB&tab=sop") settle(1.8) chk("with no SOP, the creator tab is marked unavailable", page.eval("""(() => { const a = document.querySelector('.nav-tab[data-tab="wp"]'); return !!a && a.getAttribute('aria-disabled') === 'true'; })()"""), page.eval("""(() => { const a = document.querySelector('.nav-tab[data-tab="wp"]'); return a ? (a.getAttribute('aria-disabled') || '(not set)') : 'missing'; })()""")) chk("...but it is still a real control, in the tab order", page.eval("""(() => { const a = document.querySelector('.nav-tab[data-tab="wp"]'); if (!a) return false; a.focus(); return document.activeElement === a; })()""")) page.eval("""(() => { document.querySelector('.nav-tab[data-tab="wp"]').click(); })()""") settle(1.2) chk("...clicking it does not navigate", "work-package-suite.html" in page.eval("location.pathname"), page.eval("location.pathname")) chk("...it opens the gate panel instead", page.eval("""(() => { const g = document.getElementById('wp-gate'); return !!g && getComputedStyle(g).display !== 'none'; })()""")) chk("...which says what to do about it", "SOP" in (page.eval( "(document.getElementById('wp-gate')||{}).textContent||''") or "")) # ── 6. the backlog entries logged against this file ─────────────────────────── def measurement(page, base, tok): print("\n6. the four backlog entries logged against the creator, re-measured") page.clear_cookies() page.set_cookie("wp_session", tok["root"]) # BL-001: the creator laid out 485px of content in a 390px viewport, because a # runtime