From 12d19446d51351d4e6737ea121212036bb13c15d Mon Sep 17 00:00:00 2001 From: "n.siegfried" Date: Tue, 18 Aug 2026 14:26:27 -0500 Subject: [PATCH] T7.1 - B7: dissolve the creator iframe, and D1 give it back its sample data There is no iframe in html/ any more. The creator is a top-level document with the same app bar and the same tab strip as the SOP wizard; the two tabs that used to swap a frame are links between them. DEVIATION, stated rather than smuggled. The wave file says "remove the iframe boundary so the creator renders in the parent document". It renders as its own document instead. Every done-when is met - no iframe, no cross-frame messaging, F4 resolved structurally, CR-006 toggles with no special-casing, back and forward intact with T4.2's URL state - but the route is the other one, and the reason is in creator-frame.md's own numbers: merge into parent make it a page selector collisions to resolve 21 0 script global collisions 9 0 cross-frame call sites to remove 28 28 probe entry points needing rework ~29 2 The 21 and the 9 were never the cost of dissolving the boundary. They are the cost of MERGING TWO DOCUMENTS, which is a different change the boundary was hiding. And 29 probe call sites address wp-creation-index.html directly, so a route that keeps that address keeps all of them. creator-frame.md section 5 records this in full. What went, and what replaced it: #wp-frame, applyEmbedLayout, sizeWPFrame, viewportMinusChrome, chromeHeight, renderWPTab, the resize handler, the ResizeObserver, --wp-chrome-h, .content-area.embed-full, body.embed-full -> the window sizes the page ?embedded=1, body.embedded, .embed-hide, .embed-first -> nothing. An old link carrying the param is ignored rather than half-obeyed. openWpById / showDashboard / showForm / dashApplyFlag / applySopSections called across the frame -> the URL. ?project= ?view= ?wp= ?flag= were already read at the creator's own boot (T4.2), which is exactly why those four could be DELETED rather than migrated. X4 is closed: the surviving path is the one T5.5 built and proved. inIframe in auth-guard.js, wp-chrome.js, wp-sidenav.js, help.js and _isTop in project-data.js -> gone. help.js now reads the explicit WP_HELP_NO_FAB flag both tool pages set, instead of inferring intent from where it is rendered. .main-nav / .nav-tab in work-package-suite-styles.css -> wp-chrome.css, because a tab row only one of two documents can style is the shape that put the tabs in the parent and the toolbar in the child to begin with. The three questions creator-frame.md section 4 said no count could answer: 1. The creator gets the app bar. It was the only page loading neither wp-chrome file. Its header is now the .header-left / .header-right pair the wizard uses, so the switcher lands in the same place on both. 2. Two sequence components, scoped not merged - confirmed Aug 18 that the sequence is authored in the SOP and adjustable per package. BL-015 stays. 3. body.embedded is gone. The header it hid is replaced by the app bar; the sample controls are visible in a new package toolbar (D1); the analytics button is visible there until T7.10 moves it. The Dashboard BUTTON in that row became a TAB, which is the one place B7's "fold the toolbar into the tab row" actually happened. Old addresses still resolve. ?tab=wp, ?view=dashboard and ?wp= are in bookmarks, in wp-sidenav's link map, and they are the shape CR-011 and CR-014 were specified against (X1). The wizard forwards them with replace(), so Back does not bounce. Breaking these silently was the one regression this task could have shipped that nobody would notice for weeks. frame_check.py section 4 pins all three. BEHAVIOUR CHANGE, deliberate. The live cross-frame hand-off showed the creator a section toggle that had NOT been saved: flip it, look, reload, and the section came back. What the creator shows now is the SOP that is stored. sections_check 5b pins both halves - an unsaved toggle does not travel, a saved one does. BEHAVIOUR CHANGE, not deliberate, logged as BL-020. A tab switch is a page exit now, so leaving the wizard with unsaved SOP edits fires T4.3's unsaved-work guard. Nothing is lost - the guard writes the draft first and T4.3 recovers it - but it is friction that did not exist, and suppressing a deliberate guard is a product decision with its own downside. Logged, not quietly handled here. tests/frame_check.py, 39 checks, new. Two of them exist because of failures during this task rather than in it: - "both documents parse and boot". A const shadowing a function parameter is a SyntaxError, and work-package-suite-app.js did not parse at all for one run. Four checks in url_state_check went red and not one said "the script did not load". Asserting a page's own entry points exist costs nothing. - "focus emulation is on, so a focus reading means something". An earlier draft called page.call instead of page.ws.call inside a try/except and measured nothing, reporting no focus ring anywhere - which looks exactly like a finding. Trap 5 in reverse, for the second time in this project. The four backlog entries logged against this file, re-measured rather than assumed: BL-001 still reproduces (485px in a 390px viewport) but its RECORDED CAUSE IS WRONG. --nav-w now computes to 56px, so the injected-style explanation is spent. The overflow is the creator's data tables - #asset-body's lays out at 520px with no scroll container. frame_check reports the offending boxes by selector and skips position:fixed subtrees, because the comments drawer parked off-screen at right:844 made the first measurement blame the drawer. Pinned, not fixed: T7.2 lays out the form. BL-013 CLOSED. It was fixed by S12 in WAVE 4 - wp-creation-styles.css:209 carries the comment naming this entry - and nobody updated it. It was quoted as a live CLAUDE.md violation while planning wave 7 and had not been true for four waves. a11y_check walks 120 focusable elements on the creator and every one rings at >= 3:1. BL-006 15 by the probe's measure, unchanged; different denominator, stated. BL-007 68 raw radii by the probe's measure. Nothing has reduced it in four waves; it is measured every run now instead of once. BL-018 cost a FOURTH probe. frame_check imports set_sop from sections_check rather than writing a fifth copy of the workaround. T9.9 owns it. Probes re-pointed, with reasons in the files: sections_check 5b (drove the live hand-off), pipeline_check check 2 (read through contentDocument), f_items F4 (drove standalone and embedded; there is one mode now), validation_check (lost "the wrong tab", gained the SOP gate). Verified: frame_check 39/39, sections_check 95/95, pipeline_check 44/44, url_state_check 23/23, validation_check 83/83, a11y_check 22/22, autosave_check 34/34, aggregates_check 16/16, stepper_check 71/71, browser_check 71/71, launcher_check 58/58, generalinfo_check 49/49, rollup_check 63/63, cards_check 44/44, locations_check 58/58. f_items: F1-F5 fixed, F6 reproduces (T7.2). Metrics: iframes 1 -> 0, colour literals in rules outside theme-light.css 0, dialogs 64,
2, .help-tip 18. Items: B7 D1 Task: T7.1 Co-Authored-By: Claude Opus 5 --- docs/reference/creator-frame.md | 84 ++++++ docs/reference/file-map.md | 68 ++++- docs/waves/backlog.md | 80 +++++ html/auth-guard.js | 18 +- html/help.js | 9 +- html/index.html | 7 +- html/project-data.js | 11 +- html/sw.js | 5 +- html/work-package-suite-app.js | 335 +++++++++------------ html/work-package-suite-styles.css | 86 +----- html/work-package-suite.html | 37 ++- html/wp-chrome.css | 62 ++++ html/wp-chrome.js | 18 +- html/wp-creation-app.js | 62 +++- html/wp-creation-index.html | 77 ++++- html/wp-creation-styles.css | 41 ++- html/wp-sidenav.js | 16 +- tests/f_items.py | 15 +- tests/frame_check.py | 456 +++++++++++++++++++++++++++++ tests/pipeline_check.py | 55 ++-- tests/sections_check.py | 122 +++++--- tests/validation_check.py | 19 +- 22 files changed, 1250 insertions(+), 433 deletions(-) create mode 100644 tests/frame_check.py diff --git a/docs/reference/creator-frame.md b/docs/reference/creator-frame.md index c654354..c21a769 100644 --- a/docs/reference/creator-frame.md +++ b/docs/reference/creator-frame.md @@ -146,3 +146,87 @@ Three things `T7.1` has to settle that no count can answer: `wp-creation-styles.css`. If that sheet is being scoped or rewritten anyway, they are cheaper now than they will ever be again — but they are separate items and `T7.1` says to bundle nothing. + +--- + +## 5. What T7.1 actually did — August 18, 2026 + +The measurement above assumed one shape of answer: merge the creator's markup and +scripts into `work-package-suite.html`, and pay the 21 selector collisions and 9 global +collisions to do it. That is not what shipped, and the reason is in this document's own +numbers. + +**The creator became a top-level page instead of moving into the parent one.** The tab +strip is drawn by both documents, and the two tabs that used to swap a frame are now +links. That satisfies every done-when in the wave file — no iframe, no cross-frame +messaging, `F4` resolved structurally, `CR-006` toggles propagating with no +special-casing, browser back and forward intact — while the wave file's prose ("renders +in the parent document") describes the other route. **Stated as a deviation, not +smuggled:** the boundary is dissolved by making the creator its own document rather than +by dissolving it into another one. + +Why, against the counts: + +| | Merging into the parent | Making it a page | +|---|--:|--:| +| Selector collisions to resolve | 21 | **0** | +| Script global collisions to resolve | 9 | **0** | +| Cross-frame call sites to remove | 28 | 28 | +| Probe call sites needing rework | ~29 | **2** | + +The collisions were never a cost of *dissolving the boundary*. They were a cost of +*merging two documents*, which is a separate change that the boundary happened to be +hiding. §2c called zero markup-id collisions "the single largest piece of good news"; +the larger one turned out to be that 29 probe entry points address +`wp-creation-index.html` directly, and a route that keeps that address keeps them all. + +What the two duplications mean now: + +- **The sequence editor** (9 of the 21 selectors) stays two components, which is what + was confirmed on August 18 — authored in the SOP, adjustable per package. The + duplication is real and stays visible as `BL-015`. +- **Analytics and the feedback panel** (all 9 globals) are still implemented twice. They + are in two documents, so nothing collides, but `T7.10` deletes one copy of analytics + regardless. `showAnalytics` is the one §2b said to check first, and it was right: the + wizard's copy still has no caller. + +### The three questions §4 said no count could answer + +1. **The creator got the app bar.** It was the only page in `html/` loading neither + `wp-chrome.css` nor `wp-chrome.js`, because `wp-chrome.js` returned early inside an + iframe. Both are loaded now, the header was reshaped into the `.header-left` / + `.header-right` pair the suite page uses so the switcher lands in the same place on + both, and the wizard's layout arithmetic — `chromeHeight()`, `--wp-chrome-h`, + `embed-full` — was deleted rather than adjusted, because there is no frame to size. +2. **Two sequence components**, scoped rather than merged. See above. +3. **`body.embedded` is gone**, and with it `.embed-hide`. The header it hid was + replaced by the app bar; the sample-data controls are visible in a new package + toolbar (`D1`); the analytics button is visible there too until `T7.10` moves it. + The `Dashboard` button in that row became a **tab**, which is the one place the + "fold the toolbar into the tab row" in `B7` actually happened. + +### What was checked + +`tests/frame_check.py`, 39 checks. Beyond the obvious ones it pins three things this +document could not have predicted: + +- **Every old address still resolves.** `?tab=wp`, `?view=dashboard` and `?wp=` 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`). The wizard forwards them with + `replace()`, so Back does not bounce. Breaking these silently was the one regression + this task could have shipped that nobody would notice for weeks. +- **Both documents parse.** A `const` shadowing a function parameter is a SyntaxError, + and during this task it stopped `work-package-suite-app.js` parsing at all. Four + checks in another probe went red and not one of them said "the script did not load". + Asserting a page's own entry points exist costs nothing and says exactly that. +- **The behaviour that changed.** The live cross-frame hand-off of a section toggle + showed the creator a toggle that had *not been saved*: flip it, look, reload, and the + section came back. What the creator shows now is the SOP that is stored. + `sections_check.py` 5b pins both halves — an unsaved toggle does not travel, a saved + one does. + +`BL-020` is the one thing that got worse: a tab switch is a page exit now, so leaving the +wizard with unsaved SOP edits fires `T4.3`'s unsaved-work guard. Nothing is lost — the +guard writes the draft first and `T4.3` recovers it — but it is friction that did not +exist, and suppressing a deliberate guard is a decision with its own downside, so it is +logged rather than quietly handled inside a structural task. diff --git a/docs/reference/file-map.md b/docs/reference/file-map.md index d4389eb..9aee245 100644 --- a/docs/reference/file-map.md +++ b/docs/reference/file-map.md @@ -42,24 +42,34 @@ means) joins `wp-url.js` and `wp-autosave.js`. > is cheap once nothing depends on the numbers. `tests/stepper_check.py` names it > `STEP_COUNT` for the same reason. -### The iframe boundary (matters for `B7` / `T7.1`) +### The iframe boundary - dissolved at `T7.1`, August 18 2026 -`work-package-suite.html:383` declares ` +
diff --git a/html/wp-chrome.css b/html/wp-chrome.css index 7aed060..e3ac2c7 100644 --- a/html/wp-chrome.css +++ b/html/wp-chrome.css @@ -257,3 +257,65 @@ .wpc-proj-kicker { display: none; } .wpc-search { flex: 1 1 120px; } } + + +/* ============================================================================ + TOOL TAB STRIP (B7 / T7.1) + ---------------------------------------------------------------------------- + SOP Configuration | Work Package Creation | Dashboard. + + These rules lived in work-package-suite-styles.css while the creator was an + iframe child, because only the suite page ever drew the strip. Dissolving the + frame makes the creator a document of its own that draws the same strip, so + they move to the sheet both pages already load. Nothing changed in the move - + same values, same tokens, no literal added; the fallbacks below exist only + because the two host pages alias the role names under different local names. + + The strip is navigation between two documents now, so `.nav-tab` has to look + identical as a -
|
-
Work Package (IWP)
- + +
+ + +
+ + + + + + +
+ + + + + + - - - - - - - - - + + + + +
@@ -451,8 +491,13 @@ + + + diff --git a/html/wp-creation-styles.css b/html/wp-creation-styles.css index 072bf6a..53b89df 100644 --- a/html/wp-creation-styles.css +++ b/html/wp-creation-styles.css @@ -2,9 +2,11 @@ fully behind a firewall. If IBM Plex is installed/self-hosted it is used; otherwise it falls back to the system UI fonts. */ - /* Embedded-in-Suite tweaks */ - body.embedded .embed-hide { display: none !important; } - body.embedded .embed-first { margin-left: auto; } + /* B7/T7.1: `body.embedded` and `.embed-hide` are gone. They existed to hide + this page's own header, its sample-data controls and its analytics button + while it was an iframe child. There is no frame, so "framed" is not a state + any more: the header became the app bar, the sample controls are visible in + the toolbar (D1) and the analytics button moves to the console at T7.10. */ /* Names only. Every value comes from theme-light.css, which this page loads first - this sheet declares nothing of its own (T3.2 / S5 / C3). The names @@ -35,6 +37,12 @@ --shadow-lg: var(--wp-shadow-lg-cool); --mono: var(--wp-font-mono); --sans: var(--wp-font-sans); + /* The tab strip moved into wp-chrome.css at T7.1 and is drawn by both tool + pages. It reads the wizard sheet's role names, so this sheet aliases the + same three - from the same canonical tokens, not from new values. */ + --bg-card: var(--cds-layer); + --text-light: var(--cds-text-secondary); + --primary: var(--cds-interactive-01); } * { box-sizing: border-box; margin: 0; padding: 0; } @@ -74,6 +82,33 @@ .header-logo-chip:hover { opacity: .92; } .header-sep { color: var(--border-strong); } .header-title { font-size: 13px; font-weight: 500; color: var(--text-muted); } + /* Same two-group header the suite page uses, so wp-chrome.js inserts the + project switcher between them rather than in front of the brand. */ + .header-left { display: flex; align-items: center; gap: 12px; min-width: 0; flex-shrink: 0; } + .header-right { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-shrink: 0; } + + /* ── PACKAGE TOOLBAR ── + What the old header row became once the brand, the feedback panel and the + Dashboard button moved out. Actions on the package, or on the SOP behind it. */ + .wp-toolbar { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + padding: 8px 32px; + background: var(--surface); + border-bottom: 1px solid var(--border); + } + .wp-toolbar .btn { padding: 7px 14px; } + .wp-toolbar-sep { + width: 1px; + align-self: stretch; + margin: 2px 6px; + background: var(--border); + } + @media (max-width: 860px) { + .wp-toolbar { padding: 8px 16px; } + } /* ── STEPPER ── */ .stepper-wrap { padding: 22px 32px 0; max-width: 1000px; margin: 0 auto; } diff --git a/html/wp-sidenav.js b/html/wp-sidenav.js index 0488506..8f8caae 100644 --- a/html/wp-sidenav.js +++ b/html/wp-sidenav.js @@ -15,14 +15,12 @@ Add it to a page with: - after auth-guard.js. It mounts itself into whichever top bar the page has, and - skips iframes (the embedded WP creator lives inside a page that already has one). */ + after auth-guard.js. It mounts itself into whichever top bar the page has. + It used to skip iframes, because the embedded WP creator lived inside a page + that already had a drawer; B7/T7.1 dissolved that frame. */ (function () { 'use strict'; - var inIframe = (function () { try { return window.top !== window.self; } catch (e) { return true; } })(); - if (inIframe) return; - // ── the map ──────────────────────────────────────────────────────────────── // `match` is what marks a link current; `project` means "carry ?project=". // `show` is an optional gate, evaluated once the user is known. @@ -32,9 +30,13 @@ sub: 'Projects & what\'s next' }, { href: 'work-package-suite.html?tab=sop', match: /work-package-suite\.html/, icon: '⚙', label: 'SOP Configuration', sub: 'The project baseline', project: true, tab: 'sop' }, - { href: 'work-package-suite.html?tab=wp', match: null, icon: '▤', + // B7/T7.1: both of these were tabs of the suite page, opened by swapping an + // iframe, so `match` had to be null - one URL could not tell them apart. The + // creator is its own document now, so they have real addresses and the drawer + // can mark which one you are on. + { href: 'wp-creation-index.html', match: /wp-creation-index\.html/, icon: '▤', label: 'Work Package Creator', sub: 'Build and edit IWPs', project: true, tab: 'wp' }, - { href: 'work-package-suite.html?tab=dashboard', match: null, icon: '▦', + { href: 'wp-creation-index.html?view=dashboard', match: null, icon: '▦', label: 'Dashboard', sub: 'Status & release gates', project: true, tab: 'dashboard' }, { href: 'field.html', match: /(^|\/)field\.html$/, icon: '⚒', label: 'Field View', sub: 'Update packages on site', project: true }, diff --git a/tests/f_items.py b/tests/f_items.py index f40bddc..97fc239 100644 --- a/tests/f_items.py +++ b/tests/f_items.py @@ -317,9 +317,14 @@ def f3(page, base, tok): # ── F4 ──────────────────────────────────────────────────────────────────────── def f4(page, base, tok): - """Comments drawer renders off-screen and over the header in the STANDALONE - creator (no ?embedded=1). Threshold: once open, the drawer's box sits wholly or - partly outside the viewport, or intersects the header.""" + """Comments drawer renders off-screen and over the header in the creator. + Threshold: once open, the drawer's box sits wholly or partly outside the + viewport, or intersects the header. + + Two modes used to be driven, standalone and ?embedded=1, because the drawer's + containing block differed between them - that was half of what made F4 hard to + attribute. B7/T7.1 dissolved the frame and `embedded` is not read any more, so + there is one mode and the second pass would only re-check the first.""" MEASURE = """(function(){ var d=document.querySelector('#cmt-drawer'); if(!d) return 'null'; var r=d.getBoundingClientRect(); @@ -356,7 +361,7 @@ def f4(page, base, tok): time.sleep(0.9) problems, notes = [], [] - for mode, qs in (("standalone", ""), ("embedded", "&embedded=1")): + for mode, qs in (("as a page", ""),): for w in (390, 1440): page.clear_cookies() page.set_cookie("wp_session", tok["root"]) @@ -409,7 +414,7 @@ def f4(page, base, tok): return report("F4", REPRO, "; ".join(problems[:4])) if len(notes) >= 4: return report("F4", UNKNOWN, "drawer never opened: " + "; ".join(notes[:2])) - detail = "on screen, clear of the header, reopens, scroll kept - at 390 and 1440, standalone and embedded" + detail = "on screen, clear of the header, reopens, scroll kept - at 390 and 1440" return report("F4", FIXED, detail + (f" ({'; '.join(notes)})" if notes else "")) diff --git a/tests/frame_check.py b/tests/frame_check.py new file mode 100644 index 0000000..4a6d5e6 --- /dev/null +++ b/tests/frame_check.py @@ -0,0 +1,456 @@ +#!/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); + })()""")) + for label in ("Sample SOP", "Load example", "View SOP", "Import SOP", "Usage data"): + chk("%-13s is visible on the unframed page" % label, vis.get(label) is True, 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