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=<id> 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, <div onclick> 2, .help-tip 18.
Items: B7 D1
Task: T7.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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=<id>` 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.
|
||||
|
||||
@@ -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 `<iframe id="wp-frame">` with **no `src`**.
|
||||
`work-package-suite-app.js:581` sets it at runtime to
|
||||
`wp-creation-index.html?embedded=1&project=<id>`.
|
||||
`work-package-suite.html` declared `<iframe id="wp-frame">` with no `src`, and
|
||||
`work-package-suite-app.js` set it at runtime to
|
||||
`wp-creation-index.html?embedded=1&project=<id>`. The creator was the only page loading
|
||||
**neither `wp-chrome.css` nor `wp-chrome.js`**, which is why it had no app bar and read
|
||||
as part of the wizard rather than as a page.
|
||||
|
||||
The creator is the only page that loads **neither `wp-chrome.css` nor `wp-chrome.js`** — that is
|
||||
why it has no app bar of its own and why it looks like part of the wizard. `T7.1` has to give it
|
||||
that chrome back, or deliberately not.
|
||||
**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 wizard; the `Work Package Creation` and
|
||||
`Dashboard` tabs are links between the two. `docs/reference/creator-frame.md` section 5
|
||||
records what was done and why that route was taken over merging the two documents.
|
||||
|
||||
Three scripts branch on `window.top !== window.self` and will change behaviour when the frame
|
||||
goes away:
|
||||
What a task touching this area needs to know now:
|
||||
|
||||
| Script | Line | Framed behaviour |
|
||||
|---|---|---|
|
||||
| `auth-guard.js` | 15 | redirects `window.top` to `login.html` |
|
||||
| `wp-chrome.js` | 17 | returns early, renders no chrome |
|
||||
| `help.js` | — | suppresses the Help FAB in the child |
|
||||
| Was | Is |
|
||||
|---|---|
|
||||
| `#wp-frame`, `applyEmbedLayout`, `sizeWPFrame`, `--wp-chrome-h`, `.embed-full` | gone; the window sizes the page |
|
||||
| `?embedded=1`, `body.embedded`, `.embed-hide` | gone; an old link carrying the param is ignored, not half-obeyed |
|
||||
| four cross-frame calls (`openWpById`, `showDashboard`/`showForm`, `dashApplyFlag`, `applySopSections`) | the URL: `?project=`, `?view=`, `?wp=`, `?flag=`, all read at the creator's own boot |
|
||||
| `.main-nav` / `.nav-tab` in `work-package-suite-styles.css` | `wp-chrome.css`, because both tool pages draw the strip |
|
||||
| `inIframe` branches in `auth-guard.js`, `wp-chrome.js`, `wp-sidenav.js`, `help.js`, and `_isTop` in `project-data.js` | gone; `help.js` uses the explicit `WP_HELP_NO_FAB` flag both tool pages set |
|
||||
|
||||
**Old addresses still work and must keep working.** `work-package-suite.html?tab=wp`,
|
||||
`?view=dashboard` and `?wp=<id>` are in bookmarks, in `wp-sidenav.js`'s link map, and
|
||||
they are the shape `CR-011` and `CR-014` emails were specified against (`X1`). The wizard
|
||||
forwards them to the creator with `replace()`, so Back does not bounce.
|
||||
`tests/frame_check.py` section 4 pins all three.
|
||||
|
||||
### Pages that are not what the plan implies
|
||||
|
||||
@@ -163,9 +173,20 @@ Recorded so wave 9 can prove they went down. Run from `html/` unless stated.
|
||||
| 6 | `.help-tip` badges | 15 | **15** ✓ | `grep -oE 'class="help-tip"' *.html \| wc -l` |
|
||||
| 7 | `aria-live` regions | 0 | **0** ✓ | `grep -ohE 'aria-live' *.html *.js \| wc -l` |
|
||||
| 8 | `pushState` calls | 0 | **0** ✓ | `grep -ohE 'pushState' *.html *.js \| wc -l` |
|
||||
| 9 | `<iframe>` in `html/` | 1 | **0** ✓ (`T7.1`) | `grep -oiE '<iframe' *.html \| wc -l` |
|
||||
|
||||
Dialogs by file: `wp-creation-app.js` 43 · `work-package-suite-app.js` 14 · `users.js` 10 ·
|
||||
`admin.js` 6 · `index.html` 6.
|
||||
|
||||
**At the T7.1 commit** the same commands read: dialogs **64** (the wizard's 14 are gone,
|
||||
the creator's 43 are `T7.9`'s), `<div onclick>` **2**, `<span onclick>` **2**,
|
||||
`.help-tip` **18**, colour literals in rules outside `theme-light.css` **0** in all five
|
||||
page sheets, and **iframes 0** - metric 9 above, added because `B7` is the only item in
|
||||
the plan whose completion is a count of zero.
|
||||
|
||||
Measure colour literals with comments stripped. Four of the five sheets now carry prose
|
||||
about the hex values they used to contain, and `BL-017` is the entry about a metric that
|
||||
counted its own explanation.
|
||||
`<div onclick>` by file: `work-package-suite.html` 10 · `wp-creation-index.html` 1 ·
|
||||
`work-package-suite-app.js` 1. Both `<span onclick>` are in `wp-creation-app.js`.
|
||||
|
||||
@@ -251,6 +272,21 @@ python tests/rollup_check.py # CR-004/CR-018 — codes, and totals that a
|
||||
python tests/cards_check.py # A7 — card status, footer, localization 44 checks
|
||||
```
|
||||
|
||||
Wave 7 adds one:
|
||||
|
||||
```bash
|
||||
python tests/frame_check.py # B7/T7.1/D1 - is the iframe actually gone? 39 checks
|
||||
```
|
||||
|
||||
**Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live
|
||||
cross-frame toggle hand-off, which no longer exists; it now pins the surviving path and
|
||||
the fact that an UNSAVED toggle correctly does not travel - a behaviour change, recorded
|
||||
rather than smoothed over. `pipeline_check.py` read the filter through
|
||||
`#wp-frame.contentDocument`; it reads the page. `f_items.py`'s `F4` drove two modes,
|
||||
standalone and `?embedded=1`; there is one mode now. `validation_check.py` lost its
|
||||
"loading the sample on the wrong tab" case for the same reason - there is no wrong tab
|
||||
on the wizard any more - and gained one for the SOP gate instead.
|
||||
|
||||
**Two probes were re-pointed at `T6.5`, and the reason is worth carrying forward.**
|
||||
`aggregates_check.py` matched the launcher card's exact wording (`"SOP complete"`), and it
|
||||
also waited for the status line to be non-empty — which stopped meaning "the answer has
|
||||
@@ -305,7 +341,9 @@ one.
|
||||
| — | helper-text contrast, worst case | 3.01:1 | **4.56:1** | `T4.6` |
|
||||
| 3 | `<div onclick>` | 12 | **2** | `T5.1` |
|
||||
| 1 | native dialogs app-wide | 79 | **64** | `T5.1` (2), `T5.2` (1), `T5.8` (13) |
|
||||
| 2 | …of those, in the creator | 43 | **43** | unchanged — wave 7/9 |
|
||||
| 2 | …of those, in the creator | 43 | **43** | unchanged — `T7.9` |
|
||||
| 9 | `<iframe>` in `html/` | 1 | **0** | `T7.1` |
|
||||
| 6 | `.help-tip` badges, none keyboard-reachable | 15 | **18** | went UP; `T9.5` |
|
||||
|
||||
Metrics 2, 4 and 6 (creator dialogs, `<span onclick>`, `.help-tip` badges) are wave 9's to
|
||||
move and are unchanged.
|
||||
|
||||
@@ -84,6 +84,26 @@ deliberately deferred.
|
||||
comments drawer is placed correctly relative to its containing block; the containing
|
||||
block is wrong. `T1.4` reports this as an attributed note rather than a drawer defect,
|
||||
so nobody is sent to the wrong file.
|
||||
- **Update, T7.1 - the recorded root cause no longer applies.** Measured after the
|
||||
rebuild by `tests/frame_check.py` section 6: at a 390px viewport the creator's
|
||||
`scrollWidth` is still **485** against a `clientWidth` of 390, so the overflow
|
||||
survives - but `--nav-w` now computes to **56px**, which is the media query
|
||||
winning. The injected `body{--nav-w:288px}` explanation above is spent; whatever
|
||||
fixed it, it was not this task.
|
||||
What is left is a different thing entirely: the widest in-flow boxes are the
|
||||
creator's **data tables**. `#asset-body`'s table lays out at **520px** with no
|
||||
scroll container around it, and the other card tables do the same. The probe
|
||||
reports the offending boxes by selector each run, and deliberately skips
|
||||
anything inside a `position: fixed` subtree - the comments drawer is parked
|
||||
off-screen by `translateX(100%)` and its five static children sit out at
|
||||
`right: 844`, which made the first measurement blame the drawer. That is how
|
||||
this entry got attributed to the wrong file once already; twice would be a
|
||||
pattern.
|
||||
**Not fixed here.** `T7.1` bundles nothing, and the fix is a layout decision -
|
||||
a scroll container, a stacked card at narrow widths, or fewer columns - which
|
||||
belongs with `T7.2` laying the form out again. `frame_check.py` **pins** the
|
||||
current failure, so the check turns red the moment it is fixed and whoever
|
||||
fixes it is told to close this entry.
|
||||
- **Update, T1.2:** the 1440px half of this is **resolved as a side effect**, not by intent.
|
||||
The unbreakable `#wp-usermenu` run that `T1.2` fixed was the cause of four of the five
|
||||
overflows recorded in wave 0 — launcher, SOP wizard and field view at 390px, and the
|
||||
@@ -155,6 +175,11 @@ deliberately deferred.
|
||||
- **Why not now:** retiring them moves text on every creator screen; `T3.2` forbids rendered
|
||||
change and `T7.1` re-lays-out this page anyway.
|
||||
- **Suggested wave or follow-up:** `T7.1`. See `docs/reference/tokens.md` §6a.
|
||||
- **Re-measured at T7.1, unchanged.** `frame_check.py` counts **15** half-pixel
|
||||
sizes in `wp-creation-styles.css` by `\d+\.5px`, which is the whole sheet
|
||||
rather than the font-size subset this entry counted, so the two numbers are not
|
||||
the same measurement and the difference is not a change. `T7.1` re-laid out the
|
||||
page's chrome, not its type. Carried to `T7.2`, which lays out the form.
|
||||
|
||||
### BL-007 — `--radius: 0` is contradicted 45 times in the sheet that declares it
|
||||
|
||||
@@ -167,6 +192,12 @@ deliberately deferred.
|
||||
- **Why not now:** changing 45 radii is the most visible diff available, and `T3.2` must
|
||||
produce none.
|
||||
- **Suggested wave or follow-up:** `T7.1`. See `docs/reference/tokens.md` §6c.
|
||||
- **Re-measured at T7.1: 68, not 45.** `frame_check.py` counts raw
|
||||
`border-radius:` declarations that do not resolve through a `var()`. The rise is
|
||||
the counting method rather than 23 new radii - this entry counted values, the
|
||||
probe counts declarations - but the direction is the point: nothing has reduced
|
||||
it in four waves, and it is measured every run now instead of once. Carried to
|
||||
`T7.2`.
|
||||
|
||||
### BL-008 — There is a second brand blue: `#2563d6`
|
||||
|
||||
@@ -253,6 +284,21 @@ deliberately deferred.
|
||||
- **Suggested wave or follow-up:** `T7.2`, or `T9.5` with the `C1` audit if it survives the
|
||||
rebuild. The fix is the ring `T3.4` established: `outline: 2px solid var(--cds-focus);
|
||||
outline-offset: -2px`, which `console.css`, `wp-chrome.css` and now the wizard all use.
|
||||
- **CLOSED at T7.1 - it was already fixed, and this entry was stale.**
|
||||
`wp-creation-styles.css:209` carries an `S12` comment naming this entry, and
|
||||
`:219` sets exactly the ring prescribed above. So it was closed in **wave 4**,
|
||||
by `S12`, and nobody came back to say so - the same way `BL-014`'s launcher half
|
||||
turned out to be closed by `T4.7`.
|
||||
Measured rather than read, twice: `frame_check.py` reports a focused creator
|
||||
input as `outline solid 2px`, and `a11y_check.py` walks **120 focusable elements
|
||||
on the creator** and finds every one of them ringing at 3:1 or better.
|
||||
Worth saying plainly, because this entry was quoted as a live `CLAUDE.md`
|
||||
violation while planning wave 7 and it had not been true for four waves: a
|
||||
backlog entry is a claim with a date on it. Re-measure before acting on one.
|
||||
The first thing `frame_check.py` does with focus is assert
|
||||
`document.hasFocus()`, because an earlier draft called `page.call` instead of
|
||||
`page.ws.call` inside a `try/except` and silently measured nothing at all -
|
||||
which reported "no ring" for every control and looked exactly like a finding.
|
||||
|
||||
### BL-015 — The creator's stepper tabs are still forced uppercase
|
||||
|
||||
@@ -323,6 +369,13 @@ deliberately deferred.
|
||||
- **Suggested wave or follow-up:** `T7.1`, or wave 9 with `C2` if the gate survives the
|
||||
rebuild unchanged. Either way `browser_check.py`'s fixture should adopt the `{sop, state}`
|
||||
shape so it stops being the only place this discrepancy is visible.
|
||||
- **Update, T7.1 - the gate survived, and this has now cost a fourth probe.**
|
||||
The WP tab is no longer a frame swap, but it is still gated on `sopComplete`,
|
||||
which is still `restoreSavedSOP()`'s answer. `frame_check.py` had every
|
||||
creator route land on the gate panel until it seeded a readable SOP; it now
|
||||
**imports `set_sop` from `sections_check.py`** rather than writing a fifth
|
||||
copy, so the workaround is in one place and disappears when the fixture is
|
||||
fixed. Four probes is enough evidence: `T9.9` owns it.
|
||||
|
||||
### BL-019 — A cost code that has left the list is silently blanked on edit
|
||||
|
||||
@@ -364,3 +417,30 @@ deliberately deferred.
|
||||
`T4.7` added, which post-dates this entry. So the launcher half of BL-014 is closed;
|
||||
what is left is `field.html`'s `.fld-search`, and `T9.5` should re-measure that one the
|
||||
same way rather than inheriting this entry's wording.
|
||||
|
||||
### BL-020 — Switching from the SOP wizard to the creator can now prompt to leave
|
||||
|
||||
- **Found during:** T7.1
|
||||
- **Where:** `html/wp-autosave.js:96` (the `beforeunload` guard), reached from the
|
||||
tool tabs in `html/work-package-suite.html`
|
||||
- **What:** the Work Package Creation and Dashboard tabs used to swap an iframe
|
||||
inside one document. Since `B7`/`T7.1` they are links to another document, so
|
||||
leaving the wizard with unsaved SOP edits fires `T4.3`'s unsaved-work guard and
|
||||
the browser asks whether to leave. The guard is behaving exactly as designed;
|
||||
what changed is that a routine tab switch is now a page exit.
|
||||
Nothing is lost either way - the guard writes the draft before prompting, and
|
||||
`T4.3`'s recovery restores it on return - so this is friction, not data loss.
|
||||
Note that `sopIsDirty()` compares against the fingerprint taken at load and at
|
||||
`completeSOP()`, so **typing anything at all** makes the wizard dirty until the
|
||||
SOP is completed. On a twelve-step form that is most of the time somebody spends
|
||||
on it.
|
||||
- **Why not now:** suppressing a deliberate guard for one navigation is a product
|
||||
decision with a real downside - it is the same mechanism that stops a closed tab
|
||||
losing work - and `T7.1` is forbidden to bundle anything. Found by
|
||||
`frame_check.py`, which filters the console line rather than hiding it, and says
|
||||
why in the comment.
|
||||
- **Suggested wave or follow-up:** wave 9, with `C2`. If it is to be suppressed,
|
||||
the honest version is an in-app navigation that flushes the draft and marks the
|
||||
departure intentional, not a blanket disabling of the guard. If it is to be
|
||||
kept, `T7.2`'s side navigation is the place to make saving obvious enough that
|
||||
the prompt stops being a surprise.
|
||||
|
||||
@@ -12,11 +12,12 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var inIframe = (function () { try { return window.top !== window.self; } catch (e) { return true; } })();
|
||||
|
||||
// Register the PWA service worker (caches the app shell for offline use). Only
|
||||
// from the top window; the API and writes are never cached (see sw.js).
|
||||
if (!inIframe && 'serviceWorker' in navigator) {
|
||||
// Register the PWA service worker (caches the app shell for offline use). The
|
||||
// API and writes are never cached (see sw.js). This used to be skipped inside
|
||||
// an iframe so the embedded creator did not register a second time; B7/T7.1
|
||||
// dissolved that frame and there is no longer a document in the app that is
|
||||
// not the top one.
|
||||
if ('serviceWorker' in navigator) {
|
||||
try { navigator.serviceWorker.register('/sw.js'); } catch (e) {}
|
||||
}
|
||||
|
||||
@@ -34,9 +35,10 @@
|
||||
clearTimeout(safety);
|
||||
var next = encodeURIComponent(location.pathname + location.search);
|
||||
var url = 'login.html?next=' + next;
|
||||
// If we're inside the WP-creator iframe, redirect the whole window.
|
||||
var w = inIframe ? window.top : window;
|
||||
try { w.location.replace(url); } catch (e) { window.location.replace(url); }
|
||||
// Was `inIframe ? window.top : window`, so an expired session inside the
|
||||
// embedded creator replaced the whole window rather than painting a login
|
||||
// page into a frame. No frame, no branch (B7/T7.1).
|
||||
window.location.replace(url);
|
||||
}
|
||||
|
||||
window.wpLogout = function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Shared Help center + tooltip module for the Work Package Suite.
|
||||
Included by the home page, the suite, and the embedded creator. It injects:
|
||||
Included by the home page, the SOP wizard and the work package creator. It injects:
|
||||
- tooltip styles for the .help-tip (ⓘ) component and [data-tip] hovers
|
||||
- a searchable, multi-topic Help center modal opened via window.openHelp()
|
||||
- a floating "?" launcher on any page that doesn't already have a Help button
|
||||
@@ -466,8 +466,11 @@
|
||||
|
||||
// ── floating launcher on pages without their own Help button ────────────────
|
||||
function maybeAddFab() {
|
||||
var inIframe = (function () { try { return window.top !== window.self; } catch (e) { return true; } })();
|
||||
if (inIframe || global.WP_HELP_NO_FAB) return; // suite shows the parent's button
|
||||
// Both tool pages set WP_HELP_NO_FAB and carry a Help button in the app bar
|
||||
// instead. The creator used to be covered by an iframe test rather than the
|
||||
// flag, which stopped working the moment it became a page (B7/T7.1) - so it
|
||||
// sets the flag now, and says so, rather than relying on where it is rendered.
|
||||
if (global.WP_HELP_NO_FAB) return;
|
||||
if (document.querySelector('[onclick*="openHelp"]')) return; // page already has a Help trigger
|
||||
if (document.getElementById('ui-help-fab')) return;
|
||||
var b = document.createElement('button');
|
||||
|
||||
@@ -831,10 +831,15 @@
|
||||
sub: 'past due and not closed'},
|
||||
];
|
||||
|
||||
// B7/T7.1: this pointed at the suite page, which loaded the dashboard into an
|
||||
// iframe and forwarded the filter across the boundary because the frame's src
|
||||
// carried only the project. The creator is a page, so link at it directly.
|
||||
// The old address still resolves - the suite page forwards it - but a link
|
||||
// built today should not need forwarding.
|
||||
function dashHref(projectId, flag){
|
||||
const q = new URLSearchParams({ view: 'dashboard', project: projectId });
|
||||
if(flag) q.set('flag', flag);
|
||||
return 'work-package-suite.html?' + q.toString();
|
||||
return 'wp-creation-index.html?' + q.toString();
|
||||
}
|
||||
|
||||
function renderPipeline(active){
|
||||
|
||||
@@ -339,13 +339,14 @@
|
||||
renderSyncBadge(c);
|
||||
}
|
||||
|
||||
// Tiny sync indicator (bottom-left). Rendered only in the top-level window so it
|
||||
// isn't duplicated inside the embedded creator iframe; the top window still sees
|
||||
// the iframe's queue changes via the 'storage' event below.
|
||||
var _isTop = (function () { try { return window.top === window.self; } catch (e) { return true; } })();
|
||||
// Tiny sync indicator (bottom-left). This was gated on being the top-level
|
||||
// window so it was not drawn twice - once by the suite page and again inside
|
||||
// the embedded creator. B7/T7.1 dissolved that frame, so there is one document
|
||||
// and one badge. The 'storage' listener below stays: it is what keeps two
|
||||
// TABS in step, which is a different thing and still happens.
|
||||
var _badgeHideTimer = null;
|
||||
function renderSyncBadge(c) {
|
||||
if (!_isTop || !document.body) return;
|
||||
if (!document.body) return;
|
||||
var el = document.getElementById('wp-sync-badge');
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
|
||||
@@ -55,7 +55,8 @@ self.addEventListener('activate', (e) => {
|
||||
// Why not cache-first for code: these files reference each other, and the cache
|
||||
// stores them as independent entries. Cache-first served whichever copy of each file
|
||||
// happened to be stored, so a browser could run new HTML against old CSS — which is
|
||||
// exactly how the embedded creator once collapsed to a 300x150 iframe. A page must
|
||||
// exactly how the creator once collapsed to a 300x150 box when it was an iframe
|
||||
// and its stylesheet was a version behind its markup. A page must
|
||||
// only ever run against the stylesheet and scripts it shipped with.
|
||||
const CODE_RE = /\.(html|css|js)$|\/$/i;
|
||||
|
||||
@@ -69,7 +70,7 @@ self.addEventListener('fetch', (e) => {
|
||||
const isCode = CODE_RE.test(url.pathname);
|
||||
|
||||
// Cache key WITHOUT the query string. Links inside the app carry ?project=…&tab=…,
|
||||
// and the embedded creator used to carry a cache-busting timestamp, so keying on the
|
||||
// and the creator used to carry a cache-busting timestamp in its frame src, so keying on the
|
||||
// full URL both missed every offline navigation and grew the cache without bound.
|
||||
const key = new Request(url.origin + url.pathname, { credentials: 'same-origin' });
|
||||
const fromCache = () => caches.match(key).then((c) => c || caches.match(req));
|
||||
|
||||
@@ -309,12 +309,20 @@ window.addEventListener('DOMContentLoaded',()=>{
|
||||
// fromUrl on every call: the URL already says this, so restoring it must not
|
||||
// add a history entry. Without that, the first Back after loading a deep link
|
||||
// would just return you to the same view.
|
||||
//
|
||||
// T7.1: ?tab=wp, ?view=dashboard and ?wp=<id> now describe a different
|
||||
// document. They are still honoured rather than ignored - they are in
|
||||
// bookmarks, in wp-sidenav's link map, and they are the shape the CR-011 and
|
||||
// CR-014 emails were specified against. Forward them, with replace() so the
|
||||
// address that was only ever a redirect is not a place Back can return to.
|
||||
const tab = params.get('tab');
|
||||
_deepLinkWp = params.get('wp') || '';
|
||||
const deepWp = params.get('wp') || '';
|
||||
const wantDashboard = params.get('view') === 'dashboard';
|
||||
if(wantDashboard) switchTool('dashboard', {fromUrl:true});
|
||||
else if(tab === 'wp' || tab === 'sop') switchTool(tab, {fromUrl:true});
|
||||
else if(_deepLinkWp) switchTool('wp', {fromUrl:true});
|
||||
if(wantDashboard || tab === 'wp' || deepWp){
|
||||
if(openCreator(wantDashboard ? 'dashboard' : '', {wp: deepWp, replace: true})) return;
|
||||
}
|
||||
else if(tab === 'sop') switchTool(tab, {fromUrl:true});
|
||||
stampCreatorLinks();
|
||||
const bootStep = parseInt(params.get('step'), 10);
|
||||
if(bootStep >= 1 && bootStep <= LAST_STEP && currentTool === 'sop') goToStep(bootStep, {fromUrl:true});
|
||||
}
|
||||
@@ -382,14 +390,17 @@ function initializeWPTypes(){
|
||||
}
|
||||
|
||||
// ── LOAD SAMPLE DATA ──────────────────────────────────────────────────────────
|
||||
// Context-aware: on the SOP tab it loads the sample SOP; on the WP / Dashboard
|
||||
// tab it loads the example Work Package inside the embedded creator.
|
||||
// This page's sample is the sample SOP. It used to reach into the frame and call
|
||||
// the creator's loadExample() when the WP tab was open; the creator is its own
|
||||
// page since T7.1 and carries its own sample controls in its toolbar (D1), so
|
||||
// there is nothing to reach into and nothing to duplicate here.
|
||||
//
|
||||
// S7 still counts more sample affordances than it should. T9.4 reduces them; this
|
||||
// task only stops one of them pretending to belong to a document it is not in.
|
||||
function loadSampleData(){
|
||||
if(currentTool && currentTool !== 'sop'){
|
||||
const f = document.getElementById('wp-frame');
|
||||
if(f && f.contentWindow && typeof f.contentWindow.loadExample === 'function'){ f.contentWindow.loadExample(); }
|
||||
else { wizardToast('Open the Work Package Creation tab first, then load the sample.',
|
||||
{role: 'alert'}); }
|
||||
wizardToast('The work package sample is on the Work Package Creation page, in its toolbar.',
|
||||
{role: 'status'});
|
||||
return;
|
||||
}
|
||||
// Populate Step 1
|
||||
@@ -540,180 +551,128 @@ function repopulateForm(){
|
||||
const beEl = document.getElementById('bim_enabled'); if(beEl) beEl.checked = !!state.bimEnabled;
|
||||
}
|
||||
|
||||
// ── TOOL SWITCHING ────────────────────────────────────────────────────────────
|
||||
// ── TOOL SWITCHING ────────────────────────────────────────────
|
||||
// Only one of the three tabs is a tool on this page now. `wp` and `dashboard`
|
||||
// are the creator, which is a document of its own since T7.1, so switching to
|
||||
// them is navigation - handled by the links themselves, or by openCreator() when
|
||||
// something in code asks for them.
|
||||
function switchTool(tool, opts){
|
||||
currentTool = tool;
|
||||
if(tool === 'wp' || tool === 'dashboard'){
|
||||
return openCreator(tool === 'dashboard' ? 'dashboard' : '', opts);
|
||||
}
|
||||
currentTool = 'sop';
|
||||
// S3: which tool is open is addressable state. `fromUrl` is set when we are
|
||||
// restoring because the user pressed Back - recording that as a new entry would
|
||||
// make Back appear to do nothing.
|
||||
if(typeof WPUrl !== 'undefined' && !(opts && opts.fromUrl)){
|
||||
// `flag` is the dashboard's filter (T5.3). It rides in the URL so a pipeline
|
||||
// cell is shareable, which also means it has to be cleared on the way out —
|
||||
// otherwise leaving the board and coming back re-applies a filter nobody asked
|
||||
// for a second time.
|
||||
WPUrl.push(tool === 'dashboard' ? { tab: 'wp', view: 'dashboard' }
|
||||
: { tab: tool, view: '', flag: '' });
|
||||
// `flag` was the dashboard's filter (T5.3) and `view` the board. Both belong
|
||||
// to the creator's URL now, so leaving the SOP tab clears them here rather
|
||||
// than carrying a filter nobody asked for into a page that no longer reads it.
|
||||
WPUrl.push({ tab: 'sop', view: '', flag: '' });
|
||||
}
|
||||
// 'dashboard' is a pseudo-tab: it reuses the WP tool's content (the embedded
|
||||
// creator) but opens it straight to the dashboard view.
|
||||
const isDash = (tool === 'dashboard');
|
||||
const contentTool = isDash ? 'wp' : tool;
|
||||
|
||||
// Update nav tabs
|
||||
document.querySelectorAll('.nav-tab').forEach(t=>t.classList.remove('active'));
|
||||
const tabBtn = document.querySelector(`[data-tab="${tool}"]`);
|
||||
if(tabBtn) tabBtn.classList.add('active');
|
||||
document.querySelectorAll('.nav-tab').forEach(t => {
|
||||
t.classList.remove('active'); t.removeAttribute('aria-current');
|
||||
});
|
||||
const tabBtn = document.querySelector('[data-tab="sop"]');
|
||||
if(tabBtn){ tabBtn.classList.add('active'); tabBtn.setAttribute('aria-current', 'page'); }
|
||||
|
||||
// Update content
|
||||
document.querySelectorAll('.tool').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById(`tool-${contentTool}`).classList.add('active');
|
||||
|
||||
if(contentTool === 'wp') renderWPTab(isDash);
|
||||
// Only go full-bleed when the creator is actually showing. With the SOP
|
||||
// incomplete this tab shows a short 'complete the SOP first' gate; making the
|
||||
// page unscrollable around it can clip its button off the bottom.
|
||||
applyEmbedLayout(contentTool === 'wp' && sopComplete);
|
||||
const pane = document.getElementById('tool-sop');
|
||||
if(pane) pane.classList.add('active');
|
||||
|
||||
updateStepUI();
|
||||
updateProjectDisplay();
|
||||
}
|
||||
|
||||
// The embedded creator/dashboard fills the window below the app chrome, so there
|
||||
// is ONE scrollbar (the iframe's) instead of a skinny inner pane inside a scrolling
|
||||
// page — and the creator's sticky bars have a real viewport to stick to.
|
||||
// A work package the global search asked us to open, handed to the creator on its
|
||||
// next load and then cleared.
|
||||
let _deepLinkWp = '';
|
||||
// ── WHERE THE CREATOR LIVES (B7 / T7.1) ──────────────────────────────────────
|
||||
// This block used to be 90 lines of iframe arithmetic: measure the chrome, size
|
||||
// the frame to what is left, re-measure on resize, watch the bar with a
|
||||
// ResizeObserver because it grows after the frame is already sized, and pin the
|
||||
// result with inline styles so a stale cached stylesheet could not collapse the
|
||||
// frame to the 300x150 default box. All of it existed to make one document look
|
||||
// like part of another.
|
||||
//
|
||||
// The creator is its own document now. Its size is the window.
|
||||
//
|
||||
// What replaces the four cross-frame calls is the URL, and it was already
|
||||
// built: `?project=`, `?view=` and `?wp=` are read by the creator during its own
|
||||
// boot (T4.2 / S3). That is why those calls could be deleted rather than
|
||||
// migrated - they existed only because two documents could not share a variable.
|
||||
|
||||
function applyEmbedLayout(on){
|
||||
// Below this, "fill the window" leaves nothing usable: the iframe would be shorter
|
||||
// than the creator's own sticky bars while the page itself can't scroll. Fall back
|
||||
// to normal page flow and let the page scroll instead.
|
||||
const MIN_FILL_H = 460;
|
||||
const fits = (window.innerHeight - chromeHeight()) >= MIN_FILL_H;
|
||||
const full = !!on && fits;
|
||||
const area = document.querySelector('.content-area');
|
||||
const frame = document.getElementById('wp-frame');
|
||||
if(area) area.classList.toggle('embed-full', full);
|
||||
if(frame) frame.classList.toggle('fill', full);
|
||||
document.body.classList.toggle('embed-full', full);
|
||||
sizeWPFrame(full);
|
||||
// The creator's address, carrying this page's project.
|
||||
function creatorUrl(extra){
|
||||
const sp = new URLSearchParams();
|
||||
const projId = new URLSearchParams(window.location.search).get('project')
|
||||
|| (activeProject && activeProject.id) || '';
|
||||
if(projId) sp.set('project', projId);
|
||||
Object.keys(extra || {}).forEach(k => { if(extra[k]) sp.set(k, extra[k]); });
|
||||
const q = sp.toString();
|
||||
return 'wp-creation-index.html' + (q ? '?' + q : '');
|
||||
}
|
||||
|
||||
// Size the frame with INLINE styles, not only CSS classes. Inline wins over any
|
||||
// stylesheet — including a stale cached one — so the creator can't collapse to the
|
||||
// 300x150 default iframe box if the CSS and the HTML are ever out of step.
|
||||
function sizeWPFrame(on){
|
||||
const frame = document.getElementById('wp-frame');
|
||||
if(!frame) return;
|
||||
frame.style.width = '100%';
|
||||
frame.style.border = '0';
|
||||
if(on){
|
||||
const h = viewportMinusChrome();
|
||||
document.documentElement.style.setProperty('--wp-chrome-h', (window.innerHeight - h) + 'px');
|
||||
frame.style.height = h + 'px';
|
||||
frame.style.minHeight = '0';
|
||||
} else {
|
||||
frame.style.height = '';
|
||||
frame.style.minHeight = 'calc(100vh - 200px)';
|
||||
}
|
||||
}
|
||||
|
||||
function chromeHeight(){
|
||||
const hdr = document.querySelector('.header');
|
||||
const nav = document.querySelector('.main-nav');
|
||||
return (hdr ? hdr.offsetHeight : 48) + (nav ? nav.offsetHeight : 48);
|
||||
}
|
||||
// Whatever is left of the window below the app bar + tab strip. No floor: a floor
|
||||
// taller than the remaining space pushes the frame (and the creator's fixed save bar)
|
||||
// off a window that has scrolling disabled.
|
||||
function viewportMinusChrome(){
|
||||
return Math.max(0, window.innerHeight - chromeHeight());
|
||||
}
|
||||
// Re-measure on resize, and re-decide whether full-bleed still fits.
|
||||
window.addEventListener('resize', () => {
|
||||
if(currentTool === 'wp' || currentTool === 'dashboard') applyEmbedLayout(true);
|
||||
}, {passive:true});
|
||||
// The app bar grows when wp-chrome.js injects the project switcher and search, which
|
||||
// happens after the frame has already been sized. Watch the chrome instead of relying
|
||||
// on someone resizing the window.
|
||||
try {
|
||||
const _ro = new ResizeObserver(() => {
|
||||
if(document.body.classList.contains('embed-full')) sizeWPFrame(true);
|
||||
// Write the real address onto the two tab links. They ship with a `data-nav-href`
|
||||
// rather than an `href` so a click before the project is known cannot navigate
|
||||
// somewhere wrong; once this runs they are ordinary links, which is what makes
|
||||
// middle-click, ctrl-click and "copy link address" work on them.
|
||||
function stampCreatorLinks(){
|
||||
document.querySelectorAll('a.nav-tab[data-nav-href]').forEach(a => {
|
||||
const base = a.getAttribute('data-nav-href') || '';
|
||||
const q = base.indexOf('?');
|
||||
const extra = {};
|
||||
if(q >= 0) new URLSearchParams(base.slice(q + 1)).forEach((v, k) => { extra[k] = v; });
|
||||
a.setAttribute('href', creatorUrl(extra));
|
||||
a.classList.toggle('gated', !sopComplete);
|
||||
if(sopComplete) a.removeAttribute('aria-disabled');
|
||||
else a.setAttribute('aria-disabled', 'true');
|
||||
});
|
||||
['.header', '.main-nav'].forEach(sel => { const el = document.querySelector(sel); if(el) _ro.observe(el); });
|
||||
} catch(e) { /* no ResizeObserver: the resize handler above still covers it */ }
|
||||
}
|
||||
|
||||
// Show the gate or the embedded Work Package Creator depending on SOP status.
|
||||
// wantDash=true opens the creator straight to the dashboard view.
|
||||
function renderWPTab(wantDash){
|
||||
// The gate: with no SOP there is nothing for the creator to build from. Show the
|
||||
// panel on this page rather than sending someone to a creator that would render
|
||||
// its own empty state and explain nothing. The tab stays a real control and
|
||||
// keeps focus - it says why instead of disappearing.
|
||||
function showCreatorGate(){
|
||||
currentTool = 'wp';
|
||||
document.querySelectorAll('.nav-tab').forEach(t => {
|
||||
t.classList.remove('active'); t.removeAttribute('aria-current');
|
||||
});
|
||||
const tabBtn = document.querySelector('[data-tab="wp"]');
|
||||
if(tabBtn){ tabBtn.classList.add('active'); tabBtn.setAttribute('aria-current', 'page'); }
|
||||
document.querySelectorAll('.tool').forEach(t => t.classList.remove('active'));
|
||||
const pane = document.getElementById('tool-wp');
|
||||
if(pane) pane.classList.add('active');
|
||||
const gate = document.getElementById('wp-gate');
|
||||
const frame = document.getElementById('wp-frame');
|
||||
if(!gate || !frame) return;
|
||||
if(sopComplete){
|
||||
gate.style.display = 'none';
|
||||
frame.style.display = 'block';
|
||||
const sp = new URLSearchParams(window.location.search);
|
||||
const projId = sp.get('project') || (activeProject && activeProject.id) || '';
|
||||
const wantWp = _deepLinkWp; _deepLinkWp = '';
|
||||
const dash = wantDash;
|
||||
|
||||
// The frame's identity is the PROJECT only. The view (form vs dashboard) and
|
||||
// which package to open are applied by calling into the loaded document, so
|
||||
// switching tabs never reloads it — reloading discarded unsaved form edits, made
|
||||
// the creator unreachable offline, and stored a fresh copy in the SW cache each
|
||||
// time. It's same-origin, so a direct call is fine.
|
||||
const src = 'wp-creation-index.html?embedded=1'
|
||||
+ (projId ? '&project=' + encodeURIComponent(projId) : '');
|
||||
|
||||
const applyNow = () => {
|
||||
try {
|
||||
const cw = frame.contentWindow;
|
||||
if(!cw) return;
|
||||
if(wantWp && typeof cw.openWpById === 'function' && !cw.openWpById(wantWp)){
|
||||
if(typeof cw.toast === 'function') cw.toast('That work package is not on this project.');
|
||||
}
|
||||
if(dash && typeof cw.showDashboard === 'function'){
|
||||
// T5.3: the launcher's pipeline cells link to a filtered board. The
|
||||
// creator is an iframe child whose src carries only the project, so the
|
||||
// filter has to be handed across rather than read from the child's own
|
||||
// URL — and BEFORE showDashboard(), which renders. B7/T7.1 dissolves
|
||||
// this frame and this hand-off goes with it.
|
||||
const flag = new URLSearchParams(window.location.search).get('flag') || '';
|
||||
if(typeof cw.dashApplyFlag === 'function') cw.dashApplyFlag(flag);
|
||||
cw.showDashboard();
|
||||
}
|
||||
else if(!dash && typeof cw.showForm === 'function') cw.showForm();
|
||||
} catch(e){ /* cross-document timing; nothing useful to do */ }
|
||||
};
|
||||
// Wait for the creator's data, not just its document. `load` fires before
|
||||
// pullProject() resolves, so opening a specific package straight after load
|
||||
// silently found nothing.
|
||||
const whenReady = () => {
|
||||
try {
|
||||
const cw = frame.contentWindow;
|
||||
if(cw && cw.wpCreatorReady) { applyNow(); return; }
|
||||
if(cw && cw.document) { cw.document.addEventListener('wp-creator-ready', applyNow, {once:true}); return; }
|
||||
} catch(e){}
|
||||
applyNow();
|
||||
};
|
||||
|
||||
if(frame.getAttribute('data-src') === src && frame.contentWindow){
|
||||
whenReady();
|
||||
} else {
|
||||
frame.setAttribute('data-src', src);
|
||||
frame.addEventListener('load', whenReady, {once:true});
|
||||
frame.src = src;
|
||||
}
|
||||
}else{
|
||||
gate.style.display = 'block';
|
||||
frame.style.display = 'none';
|
||||
}
|
||||
if(gate) gate.style.display = 'block';
|
||||
updateStepUI();
|
||||
updateProjectDisplay();
|
||||
}
|
||||
|
||||
// Called from completeSOP / restoreSavedSOP once an SOP is available.
|
||||
// Called by the two tab links. Returns true to let the browser navigate.
|
||||
function gateCreatorLink(ev){
|
||||
if(sopComplete) return true;
|
||||
if(ev && ev.preventDefault) ev.preventDefault();
|
||||
showCreatorGate();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open the creator, or the gate. `opts.replace` is for a deep link being
|
||||
// forwarded: the redirect is not a place anyone chose to be, so Back must not
|
||||
// land back on it.
|
||||
function openCreator(view, opts){
|
||||
if(!sopComplete){ showCreatorGate(); return false; }
|
||||
const url = creatorUrl({ view: view || '', wp: (opts && opts.wp) || '' });
|
||||
if(opts && opts.replace) window.location.replace(url);
|
||||
else window.location.assign(url);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called from completeSOP / restoreSavedSOP once an SOP is available. There is
|
||||
// no frame to re-render since T7.1; what changes is that the two creator tabs
|
||||
// stop being gated, so their links have to start working without a reload.
|
||||
function onSOPReady(){
|
||||
if(currentTool === 'wp') renderWPTab();
|
||||
stampCreatorLinks();
|
||||
if(currentTool === 'wp' && sopComplete) openCreator('', {replace:true});
|
||||
}
|
||||
|
||||
// Active project comes from the home page (?project=<id> + ProjectData.getActive()).
|
||||
@@ -1660,35 +1619,20 @@ document.addEventListener('DOMContentLoaded', function(){
|
||||
if(st) st.textContent = t.checked ? 'In use' : 'Not used';
|
||||
}
|
||||
renderSectionSummary();
|
||||
// The creator may already be loaded in the frame next door. Hand the change
|
||||
// across rather than waiting for a reload — see pushSectionsToCreator.
|
||||
pushSectionsToCreator();
|
||||
// No hand-off: the creator reads SOP.sections at its own boot (T5.5), and
|
||||
// since T7.1 there is no already-open frame to be out of date.
|
||||
});
|
||||
});
|
||||
|
||||
// X4, the sequencing note this task had to resolve: the creator is an iframe
|
||||
// child until T7.1, so the SOP it reads is the one hydrated into localStorage by
|
||||
// ProjectData.pullProject at ITS boot. A toggle flipped in the wizard afterwards
|
||||
// is invisible to an already-loaded frame.
|
||||
// X4, resolved. Section toggles reached the creator by two paths while it was an
|
||||
// iframe child: the SOP it reads at its own boot, and a live hand-off across the
|
||||
// boundary for a frame that was already open.
|
||||
//
|
||||
// Two paths, therefore, and both are needed:
|
||||
// • on the creator's own boot, applySOP() reads SOP.sections — this covers a
|
||||
// reload, a fresh tab, and the standalone creator page;
|
||||
// • while the frame is open, this hands the change straight across, the same
|
||||
// way T5.3 hands the dashboard filter across.
|
||||
// What T7.1 removes is the second path, not the first.
|
||||
function pushSectionsToCreator(){
|
||||
const frame = document.getElementById('wp-frame');
|
||||
if(!frame) return false;
|
||||
try {
|
||||
const cw = frame.contentWindow;
|
||||
if(cw && typeof cw.applySopSections === 'function'){
|
||||
cw.applySopSections(sopSections(), sopFields());
|
||||
return true;
|
||||
}
|
||||
} catch(e){ /* cross-document timing; the creator picks it up on its next boot */ }
|
||||
return false;
|
||||
}
|
||||
// T7.1 deleted the second one, because there is no longer a second document to
|
||||
// hand anything to. What is left is the path T5.5 built and proved: applySOP()
|
||||
// reads SOP.sections during the creator's boot, which covers a reload, a fresh
|
||||
// tab and a deep link equally. The creator now always boots after the toggle was
|
||||
// saved, so "already open and out of date" is not a state that exists.
|
||||
|
||||
// ── STEP GATES (A4 / S9) ──────────────────────────────────────────────────────
|
||||
// validateStep() guarded steps 1, 5 and 6 with three hand-written conditions and
|
||||
@@ -1810,8 +1754,11 @@ function goToStep(n, opts){
|
||||
if(typeof WPUrl !== 'undefined'){
|
||||
WPUrl.onChange(function(state, viaPop){
|
||||
if(!viaPop) return;
|
||||
const wantTool = state.view === 'dashboard' ? 'dashboard' : (state.tab || 'sop');
|
||||
if(wantTool !== currentTool) switchTool(wantTool, {fromUrl:true});
|
||||
// T7.1: this page only has the SOP tool. A popstate carrying ?tab=wp or
|
||||
// ?view=dashboard would once have swapped the frame; acting on it now would
|
||||
// NAVIGATE, so Back would bounce forward again and the button would appear
|
||||
// broken. Those addresses belong to the creator's own history stack, and the
|
||||
// boot redirect uses replace() precisely so no such entry is left here.
|
||||
const step = parseInt(state.step, 10);
|
||||
if(currentTool === 'sop' && step >= 1 && step !== currentStep) goToStep(step, {fromUrl:true});
|
||||
});
|
||||
@@ -2257,8 +2204,10 @@ function completeSOP(){
|
||||
|
||||
track('sop_generated', {woTypes: sop.woTypes.length, constraints: sop.constraints.length});
|
||||
|
||||
// Hand the SOP to the embedded Work Package Creator and unlock its tab (in case
|
||||
// the user stays), then return to the project home page per the requested flow.
|
||||
// Unlock the creator tabs (in case the user stays on this page), then return to
|
||||
// the project home page per the requested flow. The SOP itself is not handed
|
||||
// anywhere: it has just been written to storage and to the server, and the
|
||||
// creator reads it at its own boot - which is the only path left since T7.1.
|
||||
if(typeof onSOPReady === 'function') onSOPReady(sop);
|
||||
|
||||
wizardToast('SOP configuration complete. Returning to the project home page.');
|
||||
|
||||
@@ -155,51 +155,18 @@ body {
|
||||
position, progress and reachability; a badge in the app bar carried one third
|
||||
of that and was the only thing on the page that did. */
|
||||
|
||||
/* MAIN NAVIGATION — underline tabs */
|
||||
.main-nav {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
padding: 0 16px;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
padding: 13px 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background: none;
|
||||
color: var(--text);
|
||||
border-bottom-color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
/* MAIN NAVIGATION - moved to wp-chrome.css by B7/T7.1.
|
||||
The strip is chrome, and dissolving the creator's iframe made it a page that
|
||||
has to draw the same strip. A tab row only one of the two documents can style
|
||||
is the shape that put the tabs in the parent and the toolbar in the child. */
|
||||
|
||||
.tab-icon { font-size: 16px; }
|
||||
|
||||
/* CONTENT AREA
|
||||
The SOP wizard reads better with a bound on line length, but 1000px on a 1920
|
||||
screen wasted half the display — and it also squeezed the embedded Work Package
|
||||
Creator (an iframe living in here) into a ~930px column with its own scrollbar
|
||||
inside the page's. Wider cap for the wizard; the embedded tools go full-bleed
|
||||
(see .content-area.embed-full below). */
|
||||
screen wasted half the display. The wider cap stays; what it used to also have
|
||||
to accommodate - the creator embedded in here, squeezed into a ~930px column
|
||||
with its own scrollbar inside the page's - is a separate page since T7.1. */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
@@ -208,39 +175,11 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Work Package Creation / Dashboard: the iframe fills the window below the app
|
||||
chrome and owns the only scrollbar, so the creator's sticky save bar and
|
||||
navigator drawer position against a real viewport instead of scrolling away. */
|
||||
.content-area.embed-full {
|
||||
/* `flex: none` matters: .content-area is a column flex item with `flex: 1`, whose
|
||||
flex-basis:0% overrides `height` and leaves the used height INDEFINITE — so a
|
||||
child's `height:100%` resolves to auto and the iframe collapses to its 150px
|
||||
default. Opting out of flex sizing makes the height definite. */
|
||||
flex: none;
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
height: calc(100vh - var(--wp-chrome-h, 96px));
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content-area.embed-full > .tool.active {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0; /* let it shrink instead of overflowing the shell */
|
||||
height: 100%;
|
||||
}
|
||||
#wp-frame {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
#wp-frame.fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
/* No page scrollbar while a full-bleed tool is open — the iframe scrolls. */
|
||||
body.embed-full { overflow: hidden; }
|
||||
/* B7/T7.1: the creator's iframe was sized here - #wp-frame, .content-area.embed-full,
|
||||
body.embed-full and the --wp-chrome-h custom property that carried the measured
|
||||
app-bar height across. All of it existed so one document could fill the window
|
||||
inside another and scroll with a single scrollbar. The creator is a page now;
|
||||
the window sizes it. */
|
||||
|
||||
.tool {
|
||||
display: none;
|
||||
@@ -1006,7 +945,6 @@ body.embed-full { overflow: hidden; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header { height: auto; flex-direction: column; align-items: stretch; text-align: center; gap: 0.75rem; padding: 12px 16px; }
|
||||
.main-nav { flex-wrap: wrap; }
|
||||
.content-area { padding: 1rem; }
|
||||
.step-content { padding: 1rem; }
|
||||
.wp-type-row { grid-template-columns: 1fr; }
|
||||
|
||||
@@ -47,17 +47,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MAIN NAVIGATION -->
|
||||
<div class="main-nav">
|
||||
<!-- MAIN NAVIGATION (B7 / T7.1)
|
||||
Two of these three tabs used to swap an iframe. The creator is its own
|
||||
document now, so they are links - real ones, so middle-click and
|
||||
ctrl-click work and the browser's own back button is the back button.
|
||||
wp-chrome.js stamps ?project= onto each once the active project is known.
|
||||
|
||||
They are still gated: until the SOP is complete there is nothing for the
|
||||
creator to build from, and clicking one opens the gate panel instead of
|
||||
navigating. A gated tab keeps its focus and says why, rather than
|
||||
disappearing - see switchTool() in work-package-suite-app.js. -->
|
||||
<div class="main-nav" role="navigation" aria-label="Work package tools">
|
||||
<button class="nav-tab active" data-tab="sop" onclick="switchTool('sop')">
|
||||
SOP Configuration
|
||||
</button>
|
||||
<button class="nav-tab" data-tab="wp" onclick="switchTool('wp')">
|
||||
<a class="nav-tab" data-tab="wp" data-nav-href="wp-creation-index.html"
|
||||
onclick="return gateCreatorLink(event)">
|
||||
Work Package Creation
|
||||
</button>
|
||||
<button class="nav-tab" data-tab="dashboard" onclick="switchTool('dashboard')">
|
||||
</a>
|
||||
<a class="nav-tab" data-tab="dashboard" data-nav-href="wp-creation-index.html?view=dashboard"
|
||||
onclick="return gateCreatorLink(event)">
|
||||
Dashboard
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- CONTENT AREA -->
|
||||
@@ -485,17 +496,13 @@
|
||||
<!-- Shown until the SOP is complete -->
|
||||
<div id="wp-gate" style="padding: 3rem 2rem; text-align: center;">
|
||||
<h2>Work Package Creation</h2>
|
||||
<p style="color: var(--text-light); margin: 1rem 0;">Complete the SOP Configuration first to enable Work Package creation. Once the SOP is finished, the full creator loads here with your project defaults pre-populated.</p>
|
||||
<p style="color: var(--text-light); margin: 1rem 0;">Complete the SOP Configuration first to enable Work Package creation. Once the SOP is finished, the Work Package Creation tab opens the creator with your project defaults pre-populated.</p>
|
||||
<button class="nav-btn primary" onclick="switchTool('sop')" style="margin-top: 1rem;">Go to SOP configuration</button>
|
||||
</div>
|
||||
<!-- The real Work Package Creator, embedded once the SOP is complete -->
|
||||
<!-- Sizing stays INLINE on purpose. An iframe with no width/height falls back
|
||||
to the HTML default 300x150 box, and the service worker caches this page
|
||||
and the stylesheet separately — so a browser can hold new HTML with old
|
||||
CSS and collapse the creator to a tiny scrolling box. Inline attributes
|
||||
survive any cache mismatch; the CSS below only refines them. -->
|
||||
<iframe id="wp-frame" title="Work Package Creator"
|
||||
style="display:none; width:100%; border:0; min-height:calc(100vh - 200px)"></iframe>
|
||||
<!-- B7/T7.1: the iframe stood here. The creator is a page now
|
||||
(wp-creation-index.html), reached through the tab above, so this tab
|
||||
holds only the gate: the state where there is no SOP to build from.
|
||||
Once there is one, the tab navigates and this panel is never seen. -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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 <button> and as an <a>.
|
||||
============================================================================ */
|
||||
.main-nav {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
padding: 0 16px;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
padding: 13px 18px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background: none;
|
||||
color: var(--text);
|
||||
border-bottom-color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-tab { font-family: inherit; text-decoration: none; }
|
||||
/* Not colour alone: the current tab carries aria-current, and the weight change
|
||||
plus the 3px rule say which one it is without relying on hue. */
|
||||
.nav-tab[aria-current="page"] { color: var(--text); border-bottom-color: var(--primary); font-weight: 600; }
|
||||
/* A tab that cannot be entered yet - the WP tab before the SOP is complete. It
|
||||
stays a real, focusable control that explains itself rather than vanishing;
|
||||
the gate panel is what does the explaining. */
|
||||
.nav-tab[aria-disabled="true"] { opacity: .55; cursor: default; }
|
||||
.nav-tab[aria-disabled="true"]:hover { background: none; color: var(--text-light); }
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
Search hits GET /api/search, which scopes results to the projects the signed-in
|
||||
user may access — so this is a convenience, never a way to see another job.
|
||||
|
||||
Skipped inside an iframe: the WP creator is embedded in the suite page, and a
|
||||
second bar inside the frame would be nonsense. */
|
||||
Every page in html/ gets one. Until B7/T7.1 this script returned early inside
|
||||
an iframe, because the WP creator was embedded in the suite page and a second
|
||||
bar inside the frame would have been nonsense - which also meant the creator
|
||||
was the only page in the app without an app bar. It is a page now, and it has
|
||||
one. */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var inIframe = (function () { try { return window.top !== window.self; } catch (e) { return true; } })();
|
||||
if (inIframe) return;
|
||||
|
||||
var SEARCH_MIN = 2; // characters before we ask the server
|
||||
var DEBOUNCE_MS = 180;
|
||||
|
||||
@@ -313,12 +313,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
// A work package lives inside the suite's Creator tab, so open the suite on
|
||||
// that project with the package requested; a SOP opens the SOP tab.
|
||||
// B7/T7.1: a work package used to live inside the suite page's Creator tab,
|
||||
// so a hit opened the suite and asked it to hand the package to the frame.
|
||||
// The creator is a page; link straight at it. The old address still works -
|
||||
// the suite page forwards it - but a search result should not need forwarding.
|
||||
function hrefFor(hit) {
|
||||
if (hit.kind === 'project') return 'work-package-suite.html?project=' + encodeURIComponent(hit.id);
|
||||
if (hit.kind === 'wp') {
|
||||
return 'work-package-suite.html?tab=wp&project=' + encodeURIComponent(hit.project_id || '') +
|
||||
return 'wp-creation-index.html?project=' + encodeURIComponent(hit.project_id || '') +
|
||||
'&wp=' + encodeURIComponent(hit.id);
|
||||
}
|
||||
return 'work-package-suite.html?tab=sop&project=' + encodeURIComponent(hit.project_id || '');
|
||||
|
||||
@@ -1353,7 +1353,7 @@ function printPackage(){
|
||||
|
||||
// ── VIEWS ────────────────────────────────────────────────────────────────────
|
||||
function hideDashboard(){ const dv=document.getElementById('dashboard-view'); if(dv) dv.style.display='none'; }
|
||||
function showOutput(){ hideDashboard(); setFormChrome(false); document.querySelectorAll('.main > .card, .main > .nav-row').forEach(e=>e.style.display='none'); document.getElementById('pkg-output').style.display=''; renderSavedList(); currentView='Package View'; window.scrollTo({top:0,behavior:'smooth'}); }
|
||||
function showOutput(){ hideDashboard(); setFormChrome(false); document.querySelectorAll('.main > .card, .main > .nav-row').forEach(e=>e.style.display='none'); document.getElementById('pkg-output').style.display=''; renderSavedList(); currentView='Package View'; syncToolTabs(); window.scrollTo({top:0,behavior:'smooth'}); }
|
||||
// Populating the form establishes the clean state it is later compared against.
|
||||
function wpFormPopulated(){ setTimeout(wpMarkFormClean, 0); }
|
||||
function showForm(){
|
||||
@@ -1369,9 +1369,33 @@ function showForm(){
|
||||
buildDisciplinePicker(); renderScope();
|
||||
setFormChrome(true);
|
||||
currentView='Work Package Form';
|
||||
syncToolTabs();
|
||||
window.scrollTo({top:0,behavior:'smooth'});
|
||||
}
|
||||
|
||||
// ── TOOL TABS (B7 / T7.1) ─────────────────────────────────────────
|
||||
// The strip the suite page used to own alone. Two of its three tabs switch view
|
||||
// inside this document; the third goes back to the wizard, and carries the
|
||||
// project so the wizard opens on the same one.
|
||||
//
|
||||
// `aria-current` rather than a class alone: the active tab has to be announceable,
|
||||
// and the 3px underline is a colour cue that a screen reader cannot see.
|
||||
function syncToolTabs(){
|
||||
const link = document.querySelector('a.nav-tab[data-nav-href]');
|
||||
if(link){
|
||||
const base = link.getAttribute('data-nav-href') || '';
|
||||
link.setAttribute('href', base + (activeProjectId
|
||||
? (base.indexOf('?') >= 0 ? '&' : '?') + 'project=' + encodeURIComponent(activeProjectId)
|
||||
: ''));
|
||||
}
|
||||
const current = (currentView === 'Dashboard') ? 'dashboard' : 'wp';
|
||||
document.querySelectorAll('.nav-tab').forEach(t => {
|
||||
const on = t.getAttribute('data-tab') === current;
|
||||
t.classList.toggle('active', on);
|
||||
if(on) t.setAttribute('aria-current', 'page'); else t.removeAttribute('aria-current');
|
||||
});
|
||||
}
|
||||
|
||||
// Sticky save bar + section-nav chrome (shown only on the editable form view).
|
||||
function setFormChrome(on){
|
||||
const nav=document.getElementById('section-nav'), save=document.getElementById('sticky-save');
|
||||
@@ -1664,6 +1688,10 @@ function applySopSections(sections, fields){
|
||||
if(currentView === 'Package View' && _lastRenderedPkg) renderPackage(_lastRenderedPkg);
|
||||
return wpSections;
|
||||
}
|
||||
// Exposed for the suite page to call across the frame while T5.5 and T7.1 were
|
||||
// both in flight. Nothing calls it from outside this file any more - applySOP()
|
||||
// calls it at boot, which is the path T5.5 proved and the only one left (X4).
|
||||
// Kept on window because the section probes drive it directly.
|
||||
window.applySopSections = applySopSections;
|
||||
|
||||
// ── SAVED PACKAGES ───────────────────────────────────────────────────────────
|
||||
@@ -2154,9 +2182,10 @@ function urlSyncDashFlag(opts){
|
||||
(opts && opts.replace ? WPUrl.replace : WPUrl.push).call(WPUrl, patch);
|
||||
}
|
||||
|
||||
// Apply a flag arriving from outside — a URL, or the embedding shell forwarding
|
||||
// one across the iframe boundary. Never pushes history: the caller's own
|
||||
// navigation is what put us here.
|
||||
// Apply a flag arriving from outside. Until T7.1 that meant two things - this
|
||||
// page's own URL, or the embedding shell forwarding one across the frame because
|
||||
// the frame's src carried only the project. There is one source now: the URL.
|
||||
// Never pushes history; the caller's own navigation is what put us here.
|
||||
function dashApplyFlag(f){
|
||||
const next = DASH_FLAGS.indexOf(f) >= 0 ? f : '';
|
||||
if(dashFilter.flag === next) return next;
|
||||
@@ -2409,6 +2438,7 @@ function showDashboard(opts){
|
||||
// Ask the server every time the dashboard is opened. Counts that were correct
|
||||
// when you last looked are not evidence that they are correct now.
|
||||
dashMetrics=null; loadDashMetrics();
|
||||
syncToolTabs();
|
||||
window.scrollTo({top:0,behavior:'smooth'}); track('dashboard_open');
|
||||
}
|
||||
// ── B4: dashboard counts come from the server ────────────────────────────────
|
||||
@@ -2780,11 +2810,14 @@ function showDraftRecoveryBar(id, d){
|
||||
}
|
||||
})();
|
||||
function bootSOP(){
|
||||
// When embedded in the Suite, hide the SOP import/sample controls (SOP is injected)
|
||||
// and prefer the SOP the Suite just completed (persisted to localStorage, which
|
||||
// we've already hydrated from the server for this project).
|
||||
const params = new URLSearchParams(location.search);
|
||||
if(params.get('embedded')) document.body.classList.add('embedded');
|
||||
// Prefer the SOP the wizard just completed (persisted to localStorage, already
|
||||
// hydrated from the server for this project).
|
||||
//
|
||||
// B7/T7.1: this used to also set `body.embedded` from ?embedded=1, which hid
|
||||
// this page's own header, its two sample controls and its analytics button
|
||||
// because they duplicated the shell's. There is no shell. The controls live in
|
||||
// the toolbar and are visible (D1); the parameter is not read any more, and an
|
||||
// old link that still carries it is simply ignored rather than half-obeyed.
|
||||
try {
|
||||
const raw = localStorage.getItem(wpKey('wp_suite_sop'));
|
||||
if(raw){
|
||||
@@ -2889,6 +2922,7 @@ function bootData(){
|
||||
// re-renders itself only when the board is already open.
|
||||
dashApplyFlag(state.flag || '');
|
||||
if(currentView !== 'Dashboard') showDashboardFromUrl();
|
||||
syncToolTabs();
|
||||
return;
|
||||
}
|
||||
const want = state.wp || '';
|
||||
@@ -2899,15 +2933,19 @@ function bootData(){
|
||||
} else if(currentView === 'Dashboard'){
|
||||
hideDashboard(); setFormChrome(true); currentView='Work Package Form';
|
||||
document.querySelectorAll('.main > .card, .main > .nav-row').forEach(e=>e.style.display='');
|
||||
syncToolTabs();
|
||||
}
|
||||
});
|
||||
}
|
||||
initAutosave();
|
||||
wpMarkFormClean();
|
||||
syncToolTabs();
|
||||
track('app_open');
|
||||
// The embedding shell needs to know when the packages are actually in hand: the
|
||||
// frame's `load` event fires long before pullProject() resolves, so anything that
|
||||
// acts on a specific package has to wait for this instead.
|
||||
// Kept after T7.1, with a different reader. It existed because the frame's
|
||||
// `load` event fired long before pullProject() resolved, so the shell could not
|
||||
// act on a specific package until this said the data was in hand. There is no
|
||||
// shell now - but the probes wait on exactly this signal for exactly the same
|
||||
// reason, and a page that says when its data has landed is worth having.
|
||||
window.wpCreatorReady = true;
|
||||
try { document.dispatchEvent(new CustomEvent('wp-creator-ready')); } catch(e){}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<meta name="theme-color" content="#161616">
|
||||
<link rel="stylesheet" href="theme-light.css">
|
||||
<!-- B7/T7.1: the app bar. This page used to load neither of wp-chrome's two
|
||||
files because it was an iframe child and wp-chrome.js returned early inside
|
||||
one. It is a page now. -->
|
||||
<link rel="stylesheet" href="wp-chrome.css">
|
||||
<link rel="stylesheet" href="wp-creation-styles.css">
|
||||
<link rel="stylesheet" href="wp-sidenav.css">
|
||||
</head>
|
||||
@@ -27,29 +31,65 @@
|
||||
|
||||
<div class="loading-overlay" id="loadingOverlay"><div class="spinner"></div><div class="loading-text">Saving work package…</div></div>
|
||||
|
||||
<!-- APP BAR (B7 / T7.1)
|
||||
This page loaded neither wp-chrome.css nor wp-chrome.js while it was an
|
||||
iframe child - it was the only page in html/ without an app bar, which is
|
||||
what made it read as part of the wizard rather than as a page. It has one
|
||||
now, in the same .header-left / .header-right shape the suite page uses, so
|
||||
wp-chrome.js inserts the project switcher and search at the same place on
|
||||
both. -->
|
||||
<div class="header">
|
||||
<!-- The logo chip, not the mono wordmark this page used to carry: every other page
|
||||
in the suite shows the chip, and the creator was the only one spelling the brand
|
||||
out in monospace (T2.2). -->
|
||||
<div class="logo-wrap embed-hide">
|
||||
<div class="header-left">
|
||||
<a href="index.html" class="header-logo-chip" title="Work Package Suite home">
|
||||
<span class="wp-logo-chip"><img src="prime-controls-logo.jpg" alt="Prime Controls"></span>
|
||||
</a>
|
||||
<div class="header-title">Work Package Suite</div>
|
||||
<button id="dev-toggle" class="dev-toggle" onclick="toggleDevMode()" title="dev mode" aria-label="dev mode"></button>
|
||||
</div>
|
||||
<div class="header-sep embed-hide">|</div>
|
||||
<div class="header-title">Work Package (IWP)</div>
|
||||
<button class="btn btn-ghost embed-hide" style="margin-left:auto;padding:7px 16px" onclick="document.getElementById('sop-import').click()">⤒ Import SOP</button>
|
||||
<!-- wp-chrome.js inserts the project switcher and global search here. -->
|
||||
<div class="header-right">
|
||||
<button class="btn btn-ghost" id="comments-btn" style="padding:7px 16px" onclick="toggleComments()">Feedback <span class="cbadge-total" id="cbadge-total" style="display:none">0</span></button>
|
||||
<button class="btn btn-ghost" style="padding:7px 16px" onclick="openHelp()">Help</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOOL TABS (B7 / T7.1)
|
||||
The tab row used to live in the parent document and the toolbar in this one,
|
||||
which is the thing B7 says makes folding either into the other impossible.
|
||||
Both are here now. `Dashboard` was a toolbar button; it is a tab, because
|
||||
that is what it always was.
|
||||
|
||||
SOP Configuration is an <a> because it goes to another document; the other
|
||||
two are <button>s because they switch view inside this one. wp-chrome.js
|
||||
stamps ?project= onto the link once the active project is known. -->
|
||||
<div class="main-nav" role="navigation" aria-label="Work package tools">
|
||||
<a class="nav-tab" data-nav-href="work-package-suite.html?tab=sop">SOP Configuration</a>
|
||||
<button class="nav-tab" data-tab="wp" onclick="showForm()">Work Package Creation</button>
|
||||
<button class="nav-tab" data-tab="dashboard" onclick="showDashboard()">Dashboard</button>
|
||||
</div>
|
||||
|
||||
<!-- PACKAGE TOOLBAR
|
||||
What is left of the old header once the brand, the feedback panel and the
|
||||
Dashboard button have found their proper homes: actions on the package or on
|
||||
the SOP behind it. `Sample SOP` and `Load example` are visible here rather
|
||||
than hidden by body.embedded (D1). They are still two affordances under two
|
||||
names, which is S7 and is T9.4's to reduce - this task makes them reachable,
|
||||
not fewer. -->
|
||||
<div class="wp-toolbar" id="wp-toolbar">
|
||||
<button class="btn btn-ghost" onclick="newPackage()">+ New</button>
|
||||
<button class="btn btn-ghost" onclick="duplicateWP()">Duplicate</button>
|
||||
<button class="btn btn-ghost" onclick="showHistoryCurrent()" title="Change history for this work package">History</button>
|
||||
<span class="wp-toolbar-sep" aria-hidden="true"></span>
|
||||
<button class="btn btn-ghost" onclick="openSopModal()">View SOP</button>
|
||||
<button class="btn btn-ghost" onclick="document.getElementById('sop-import').click()">Import SOP</button>
|
||||
<input type="file" id="sop-import" accept="application/json" style="display:none" onchange="importSOP(event)">
|
||||
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="loadSampleSOP()">⤓ Sample SOP</button>
|
||||
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="openSopModal()">👁 View SOP</button>
|
||||
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="loadExample()">★ Load example</button>
|
||||
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="showDashboard()">📊 Dashboard</button>
|
||||
<button class="btn btn-ghost embed-first" style="padding:7px 16px" onclick="newPackage()">+ New</button>
|
||||
<button class="btn btn-ghost" style="padding:7px 16px" onclick="duplicateWP()">⧉ Duplicate</button>
|
||||
<button class="btn btn-ghost" style="padding:7px 16px" onclick="showHistoryCurrent()" title="Change history for this work package">🕘 History</button>
|
||||
<button class="btn btn-ghost embed-hide" id="comments-btn" style="padding:7px 16px" onclick="toggleComments()">💬 Comments <span class="cbadge-total" id="cbadge-total" style="display:none">0</span></button>
|
||||
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="showAnalytics()">▤ Usage data</button>
|
||||
<button class="btn btn-ghost" onclick="loadSampleSOP()">Sample SOP</button>
|
||||
<button class="btn btn-ghost" onclick="loadExample()">Load example</button>
|
||||
<span class="wp-toolbar-sep" aria-hidden="true"></span>
|
||||
<!-- T7.10 moves this to the admin console and deletes one of the two
|
||||
implementations. It stays visible here until then rather than being
|
||||
deleted by the task that dissolved the frame. -->
|
||||
<button class="btn btn-ghost" onclick="showAnalytics()">Usage data</button>
|
||||
</div>
|
||||
|
||||
<div class="dev-banner" id="dev-banner" style="display:none">⚙ DEV MODE — usage tracking paused. This session's actions are not being recorded.</div>
|
||||
@@ -451,8 +491,13 @@
|
||||
|
||||
<script src="feedback-config.js"></script>
|
||||
<script src="project-data.js"></script>
|
||||
<!-- The Help button in the app bar is this page's entry point, the same as the
|
||||
suite page. help.js used to skip its floating button here because it saw an
|
||||
iframe; that test is gone, so say so explicitly instead. -->
|
||||
<script>window.WP_HELP_NO_FAB = true;</script>
|
||||
<script src="help.js"></script>
|
||||
<script src="wp-creation-app.js"></script>
|
||||
<script src="wp-chrome.js"></script>
|
||||
<script src="wp-sidenav.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
Add it to a page with:
|
||||
<link rel="stylesheet" href="wp-sidenav.css">
|
||||
<script src="wp-sidenav.js"></script>
|
||||
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 },
|
||||
|
||||
@@ -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 ""))
|
||||
|
||||
|
||||
|
||||
456
tests/frame_check.py
Normal file
456
tests/frame_check.py
Normal file
@@ -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 "<iframe" in body.lower():
|
||||
framed.append(f)
|
||||
chk("no page declares an iframe", not framed, framed)
|
||||
|
||||
# The four APIs that only mean something across a document boundary.
|
||||
banned = {
|
||||
"contentWindow": "reaching into a child document",
|
||||
"contentDocument": "reading a child document",
|
||||
"window.top": "asking whether we are the top document",
|
||||
"window.parent": "reaching out to a parent document",
|
||||
}
|
||||
hits = {}
|
||||
for f in scripts + pages:
|
||||
body = strip_comments(read(f), "html" if f.endswith(".html") else "js")
|
||||
for api, why in banned.items():
|
||||
if api in body:
|
||||
hits.setdefault(api + " (" + why + ")", []).append(f)
|
||||
chk("no cross-frame API is called anywhere", not hits, hits)
|
||||
|
||||
# The embedding flag and the class set it drove.
|
||||
flags = {}
|
||||
for f in scripts + pages:
|
||||
body = strip_comments(read(f), "html" if f.endswith(".html") else "js")
|
||||
for token in ("embedded=1", "embed-hide", "embed-first", "body.embedded", "wp-frame"):
|
||||
if token in body:
|
||||
flags.setdefault(token, []).append(f)
|
||||
chk("the embedding flag and its classes are gone", not flags, flags)
|
||||
|
||||
# And the sizing machinery that existed only to make a frame fill a window.
|
||||
suite = strip_comments(read("work-package-suite-app.js"), "js")
|
||||
gone = [n for n in ("applyEmbedLayout", "sizeWPFrame", "viewportMinusChrome",
|
||||
"renderWPTab", "pushSectionsToCreator")
|
||||
if n in suite]
|
||||
chk("...and so is the frame-sizing machinery", not gone, gone)
|
||||
|
||||
# The creator was the only page loading neither of wp-chrome's two files.
|
||||
creator = read("wp-creation-index.html")
|
||||
chk("the creator loads the shared app bar, like every other page",
|
||||
"wp-chrome.css" in creator and "wp-chrome.js" in creator)
|
||||
chk("...and the tab strip lives in the sheet both tool pages load",
|
||||
".nav-tab" in read("wp-chrome.css")
|
||||
and ".nav-tab {" not in read("work-package-suite-styles.css"))
|
||||
|
||||
|
||||
# ── 2. both documents boot ────────────────────────────────────────────────────
|
||||
def boot_checks(page, base, tok):
|
||||
print("\n2. both documents parse and boot")
|
||||
for label, path, probe in (
|
||||
("the SOP wizard", "/work-package-suite.html?project=projA&tab=sop",
|
||||
"typeof switchTool === 'function' && typeof goToStep === 'function'"
|
||||
" && typeof openCreator === 'function'"),
|
||||
("the creator", "/wp-creation-index.html?project=projA",
|
||||
"typeof showDashboard === 'function' && typeof newPackage === 'function'"
|
||||
" && typeof syncToolTabs === 'function'"),
|
||||
):
|
||||
page.clear_cookies()
|
||||
page.set_cookie("wp_session", tok["root"])
|
||||
page.viewport(1440)
|
||||
page.goto(base + path)
|
||||
settle(1.6)
|
||||
chk("%s's own entry points are defined" % label, page.eval(probe) is True,
|
||||
"script did not parse; see js_errors below")
|
||||
# `beforeunload` is not a fault. Leaving a page with unsaved SOP edits is
|
||||
# supposed to warn, and headless blocks the panel because there was no user
|
||||
# gesture. It IS a behaviour change T7.1 introduced - a tab switch used to
|
||||
# stay inside one document and never prompt - and it is recorded as BL-020
|
||||
# rather than quietly suppressed here.
|
||||
errs = [e for e in page.js_errors()
|
||||
if "favicon" not in e and "404" not in e and "beforeunload" not in e]
|
||||
chk("...and it logged no script error", not errs, errs[:2])
|
||||
|
||||
|
||||
# ── 3. the creator is a page ──────────────────────────────────────────────────
|
||||
def page_checks(page, base, tok):
|
||||
print("\n3. the creator is a page, not a panel")
|
||||
page.clear_cookies()
|
||||
page.set_cookie("wp_session", tok["root"])
|
||||
page.viewport(1440)
|
||||
page.goto(base + "/wp-creation-index.html?project=projA")
|
||||
chk("the creator boots", wait_creator(page))
|
||||
settle(1.4)
|
||||
|
||||
chk("it hosts no iframe", page.eval("document.querySelectorAll('iframe').length") == 0)
|
||||
chk("the app bar mounted", page.eval("!!document.querySelector('.wp-chrome')"))
|
||||
chk("...with the project switcher in it",
|
||||
page.eval("!!document.querySelector('.wp-chrome .wpc-proj-name, .wp-chrome button')"))
|
||||
chk("the navigation drawer mounted", page.eval("!!document.querySelector('#wp-sidenav')"))
|
||||
chk("the tab strip is present", page.eval(
|
||||
"document.querySelectorAll('.main-nav .nav-tab').length") == 3,
|
||||
page.eval("document.querySelectorAll('.main-nav .nav-tab').length"))
|
||||
chk("...marking Work Package Creation as the current one", page.eval("""(() => {
|
||||
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=<id> 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 <style> injected --nav-w:288px with no media query and beat the
|
||||
# stylesheet's own breakpoint.
|
||||
page.viewport(390, 900, mobile=True)
|
||||
page.goto(base + "/wp-creation-index.html?project=projA")
|
||||
wait_creator(page)
|
||||
settle(1.4)
|
||||
over = json.loads(page.eval("""JSON.stringify({
|
||||
scroll: document.documentElement.scrollWidth,
|
||||
client: document.documentElement.clientWidth,
|
||||
navw: getComputedStyle(document.body).getPropertyValue('--nav-w').trim(),
|
||||
})"""))
|
||||
print(" BL-001 390px: scrollWidth %s vs clientWidth %s, --nav-w %s"
|
||||
% (over["scroll"], over["client"], over["navw"] or "(unset)"))
|
||||
if over["scroll"] > over["client"] + 2:
|
||||
print(" still reproduces. Widest boxes: %s" % page.eval(WIDEST_JS))
|
||||
# Pinned, not fixed. BL-001 says to verify it at T7.1 and give it its own item
|
||||
# if it survives the rebuild; T7.1 says to bundle nothing into this diff. So
|
||||
# this asserts what is true TODAY and turns red the moment T7.2 lays the form
|
||||
# out again - which is the point of pinning rather than printing.
|
||||
chk("BL-001 is pinned: the creator still overflows at 390px, so this check "
|
||||
"fails when it is fixed",
|
||||
over["scroll"] > over["client"] + 2, over)
|
||||
|
||||
# BL-013: outline:none on every input, replaced by a 3px #edf5ff glow on white -
|
||||
# a 1.05:1 edge. T7.2 owns the fix; this records whether the rebuild changed it,
|
||||
# so T7.2 inherits a measurement instead of an assumption.
|
||||
page.viewport(1440)
|
||||
page.goto(base + "/wp-creation-index.html?project=projA")
|
||||
wait_creator(page)
|
||||
settle(1.2)
|
||||
# Without this the headless document is not focused, :focus-visible never
|
||||
# matches, and EVERY control reports no ring - a reading that looks like a
|
||||
# finding and is an artefact. It is `page.ws.call`, not `page.call`: an earlier
|
||||
# draft of this probe used the latter inside a try/except and silently measured
|
||||
# nothing at all, which is worse than not measuring.
|
||||
page.ws.call("Emulation.setFocusEmulationEnabled", {"enabled": True})
|
||||
chk("focus emulation is on, so a focus reading means something",
|
||||
page.eval("document.hasFocus()") is True)
|
||||
ring = page.eval("""(() => {
|
||||
const el = document.querySelector('#wp_subject, .main input[type=text]');
|
||||
if (!el) return 'no input found';
|
||||
el.focus();
|
||||
const cs = getComputedStyle(el);
|
||||
return 'outline ' + cs.outlineStyle + ' ' + cs.outlineWidth
|
||||
+ ' | box-shadow ' + cs.boxShadow
|
||||
+ ' | focused=' + (document.activeElement === el);
|
||||
})()""")
|
||||
print(" BL-013 focused creator input: %s" % ring)
|
||||
|
||||
sheet = read("wp-creation-styles.css")
|
||||
halves = len(re.findall(r"\b\d+\.5px", sheet))
|
||||
radii = len(re.findall(r"border-radius:\s*(?!var\()", sheet))
|
||||
print(" BL-006 half-pixel font sizes in wp-creation-styles.css: %d" % halves)
|
||||
print(" BL-007 raw border-radius values in the same sheet: %d" % radii)
|
||||
return {"bl001": over, "bl013": ring, "bl006": halves, "bl007": radii}
|
||||
|
||||
|
||||
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-frame-")
|
||||
db_path = os.path.join(tmpdir, "check.db")
|
||||
server = None
|
||||
try:
|
||||
tok = seed(db_path)
|
||||
set_sop(db_path, {}) # a SOP the app can actually 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 iframe - B7 / T7.1\nTarget: %s" % base)
|
||||
|
||||
source_checks()
|
||||
|
||||
browser = cdp.Browser(exe)
|
||||
page = browser.page()
|
||||
try:
|
||||
boot_checks(page, base, tok)
|
||||
page_checks(page, base, tok)
|
||||
address_checks(page, base, tok)
|
||||
gate_checks(page, base, tok)
|
||||
measurement(page, base, tok)
|
||||
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 creator is a page.")
|
||||
return 0
|
||||
finally:
|
||||
if server is not None:
|
||||
try:
|
||||
server.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -14,9 +14,11 @@ strip still report the server's.
|
||||
4. the strip announces updates via aria-live, because it refreshes in place
|
||||
5. a failed request is an error, not four zeros and not a remembered number
|
||||
|
||||
Check 2 is the one with a seam in it: the dashboard is an iframe CHILD of the SOP
|
||||
page, so the filter has to cross that boundary. The probe follows the link and
|
||||
reads the filter inside the frame, rather than trusting that the URL was built.
|
||||
Check 2 used to be the one with a seam in it: the dashboard was an iframe CHILD
|
||||
of the SOP page, so the filter had to cross that boundary and the probe read it
|
||||
inside the frame. B7/T7.1 dissolved the frame and the cell links straight at the
|
||||
creator, so the filter arrives in the page's own URL. The check still reads the
|
||||
rendered board rather than trusting that the URL was built.
|
||||
|
||||
Exit 0 all passed, 1 a failure, 2 could not run.
|
||||
"""
|
||||
@@ -199,32 +201,32 @@ def run(page, base, tok):
|
||||
sorted(h.split("flag=")[1] for h in hrefs if "flag=" in h)
|
||||
== ["onhold", "overdue", "ready"], hrefs)
|
||||
|
||||
print(" ...and the filter survives the trip into the creator's iframe")
|
||||
print(" ...and the filter arrives, on a page rather than in a frame")
|
||||
onhold_href = [h for h in hrefs if "flag=onhold" in h][0]
|
||||
page.goto(base + "/" + onhold_href.lstrip("/"))
|
||||
for _ in range(40):
|
||||
got = page.eval("""(() => {
|
||||
const f = document.getElementById('wp-frame');
|
||||
try { return f && f.contentWindow && f.contentWindow.wpCreatorReady ? 'ready' : ''; }
|
||||
catch (e) { return 'x'; }
|
||||
})()""")
|
||||
if got == "ready":
|
||||
if page.eval("!!window.wpCreatorReady"):
|
||||
break
|
||||
time.sleep(0.3)
|
||||
settle(2.0)
|
||||
# Read the FRAME'S DOM, not its globals. `dashFilter` and `currentView` are
|
||||
# declared with let in a classic script, so they are not properties of window
|
||||
# and a cross-frame read of them comes back undefined — which looks exactly
|
||||
# like a filter that was never applied. The board itself is the evidence.
|
||||
# This used to reach through `#wp-frame`.contentDocument, and had to: the
|
||||
# dashboard was an iframe CHILD of the SOP page, so the filter was handed
|
||||
# across rather than read from the child's own URL, and the only honest place
|
||||
# to check it was inside the frame. B7/T7.1 dissolved the frame - the link now
|
||||
# opens the creator directly and the filter arrives in its own URL, so this is
|
||||
# an ordinary read of an ordinary page.
|
||||
#
|
||||
# What has NOT changed is why the BOARD is read rather than the globals:
|
||||
# `dashFilter` and `currentView` are declared with let in a classic script, so
|
||||
# they are not properties of window. The board itself is still the evidence.
|
||||
inner = json.loads(page.eval("""(() => {
|
||||
try {
|
||||
const d = document.getElementById('wp-frame').contentDocument;
|
||||
const dv = d.getElementById('dashboard-view');
|
||||
const active = d.querySelector('.dash-metric.dm-active .dm-label');
|
||||
// The board only — NOT the gating panel next to it, which is server-derived
|
||||
const dv = document.getElementById('dashboard-view');
|
||||
const active = document.querySelector('.dash-metric.dm-active .dm-label');
|
||||
// The board only - NOT the gating panel next to it, which is server-derived
|
||||
// and not filtered. Counting both would let a two-package gating list pass
|
||||
// this check whatever the filter did.
|
||||
const panel = [...d.querySelectorAll('.dash-panel')].find(p => {
|
||||
const panel = [...document.querySelectorAll('.dash-panel')].find(p => {
|
||||
const t = p.querySelector('.dash-panel-title');
|
||||
return t && /^Work packages/.test(t.textContent.trim());
|
||||
});
|
||||
@@ -236,14 +238,16 @@ def run(page, base, tok):
|
||||
shown: !!dv && getComputedStyle(dv).display !== 'none',
|
||||
active: active ? active.textContent.trim() : null,
|
||||
rows: rows,
|
||||
framed: !!document.getElementById('wp-frame'),
|
||||
});
|
||||
} catch (e) { return JSON.stringify({error: String(e)}); }
|
||||
})()"""))
|
||||
chk("following an On hold cell opens the dashboard", inner.get("shown") is True, inner)
|
||||
chk("...with the on-hold tile already the active filter, inside the frame",
|
||||
chk("...with the on-hold tile already the active filter",
|
||||
(inner.get("active") or "").lower() == "on hold", inner)
|
||||
chk("...and the board listing only the held package, not all four",
|
||||
inner.get("rows") == ["WP03-HOLD"], inner)
|
||||
chk("...and no iframe was involved (B7/T7.1)", inner.get("framed") is False, inner)
|
||||
|
||||
print("\n3. a project with no work packages says so, in a sentence")
|
||||
visit("/index.html?project=projEmpty")
|
||||
@@ -254,8 +258,15 @@ def run(page, base, tok):
|
||||
chk("...an explanation is", bool(empty_txt.strip()), repr(empty_txt))
|
||||
chk("...naming the situation rather than showing four zeros",
|
||||
"no work packages" in empty_txt.lower(), repr(empty_txt.strip()[:90]))
|
||||
chk("...and offering somewhere to go", page.eval(
|
||||
"!!document.querySelector('#pipeline-strip .pipe-empty a[href*=\"work-package-suite\"]')"))
|
||||
# Re-pointed at T7.1, not relaxed. The link said "Open the creator" and went to
|
||||
# the suite page, because the creator was a tab of it. It is a page now, so the
|
||||
# link goes there directly - and the check still demands a link to the place
|
||||
# the sentence offers, rather than merely a link.
|
||||
chk("...and offering somewhere to go, which is the creator", page.eval(
|
||||
"!!document.querySelector('#pipeline-strip .pipe-empty a[href*=\"wp-creation-index\"]')"),
|
||||
page.eval("(document.querySelector('#pipeline-strip .pipe-empty a')||{}).getAttribute"
|
||||
" ? document.querySelector('#pipeline-strip .pipe-empty a').getAttribute('href')"
|
||||
" : '(no link)'"))
|
||||
chk("no zero is rendered as a headline number anywhere in the strip",
|
||||
page.eval("document.querySelectorAll('#pipeline-strip .pipe-num').length") == 0)
|
||||
|
||||
|
||||
@@ -15,10 +15,12 @@ replaces.
|
||||
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.
|
||||
Check 5 is the one X4 warned about. The creator WAS an iframe child until T7.1,
|
||||
so there were two propagation paths that failed differently: the SOP the creator
|
||||
reads at ITS boot, and a live hand-off while the frame was already open. T7.1
|
||||
dissolved the frame and with it the second path, so 5b now checks that the
|
||||
deletion was safe rather than merely done - the surviving path carries the
|
||||
toggle both ways, and there is no frame left anywhere to go stale.
|
||||
|
||||
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
|
||||
@@ -321,54 +323,92 @@ def run(page, base, tok, db_path):
|
||||
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)
|
||||
print("\n5b. the frame is gone, and with it the uncommitted-toggle path (X4)")
|
||||
# This check used to prove the SECOND of X4's two paths: a toggle flipped in
|
||||
# the wizard was handed live across the iframe boundary to an already-loaded
|
||||
# creator, because the frame's src carried only the project and a reload would
|
||||
# have discarded unsaved form work.
|
||||
#
|
||||
# B7/T7.1 deleted that path by deleting the boundary, and that is a BEHAVIOUR
|
||||
# CHANGE, not only a refactor. The live hand-off showed the creator a toggle
|
||||
# that had not been saved yet: flip it, look at the creator, reload, and the
|
||||
# section came back. What the creator shows now is the SOP that is actually
|
||||
# stored. Checks 1 through 3 above already prove the surviving path carries a
|
||||
# SAVED toggle in both directions; what is pinned here is the rest of it -
|
||||
# there is no frame anywhere, the tab is a real link, and an UNSAVED toggle
|
||||
# correctly does not travel.
|
||||
page.goto(base + "/work-package-suite.html?project=projA&tab=sop&step=12")
|
||||
settle(1.6)
|
||||
chk("the wizard page hosts no iframe at all",
|
||||
page.eval("document.querySelectorAll('iframe').length") == 0,
|
||||
page.eval("document.querySelectorAll('iframe').length"))
|
||||
chk("...and the Work Package tab is a link to the creator, not a frame swap",
|
||||
page.eval("""(() => {
|
||||
const a = document.querySelector('.nav-tab[data-tab="wp"]');
|
||||
return !!a && a.tagName === 'A' && (a.getAttribute('href') || '')
|
||||
.indexOf('wp-creation-index.html') === 0;
|
||||
})()"""),
|
||||
page.eval("""(() => { const a = document.querySelector('.nav-tab[data-tab="wp"]');
|
||||
return a ? a.tagName + ' ' + (a.getAttribute('href')||'(no href)') : 'missing'; })()"""))
|
||||
chk("...carrying the project, so the link works on a cold browser",
|
||||
"project=projA" in (page.eval("""(() => {
|
||||
const a = document.querySelector('.nav-tab[data-tab="wp"]');
|
||||
return a ? (a.getAttribute('href') || '') : ''; })()""") or ""))
|
||||
chk("...and the Dashboard tab is a link to the same page's board",
|
||||
page.eval("""(() => {
|
||||
const a = document.querySelector('.nav-tab[data-tab="dashboard"]');
|
||||
const h = a ? (a.getAttribute('href') || '') : '';
|
||||
return a && a.tagName === 'A' && h.indexOf('wp-creation-index.html') === 0
|
||||
&& h.indexOf('view=dashboard') > 0;
|
||||
})()"""),
|
||||
page.eval("""(() => { const a = document.querySelector('.nav-tab[data-tab="dashboard"]');
|
||||
return a ? a.tagName + ' ' + (a.getAttribute('href')||'(no href)') : 'missing'; })()"""))
|
||||
|
||||
# Flip Assets off WITHOUT completing the SOP, then follow the tab.
|
||||
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; }
|
||||
settle(0.6)
|
||||
chk("Assets reads as on in the wizard to begin with", page.eval("""(() => {
|
||||
const b = document.querySelector('#section-toggles input[data-section="assets"]');
|
||||
return !!b && b.checked;
|
||||
})()"""))
|
||||
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.6)
|
||||
chk("...and the wizard says so, in the summary line", page.eval("""(() => {
|
||||
const el = document.getElementById('section-summary');
|
||||
return !!el && /assets/i.test(el.textContent || '');
|
||||
})()"""), page.eval("(document.getElementById('section-summary')||{}).textContent||''"))
|
||||
|
||||
page.eval("""(() => { document.querySelector('.nav-tab[data-tab="wp"]').click(); })()""")
|
||||
settle(0.8)
|
||||
chk("turning Assets off reaches the already-loaded frame, with no reload",
|
||||
chk("following the tab lands on the creator", wait_creator(page))
|
||||
settle(1.2)
|
||||
chk("...and the creator page hosts no iframe either",
|
||||
page.eval("document.querySelectorAll('iframe').length") == 0)
|
||||
chk("...showing Assets, because the toggle was never saved",
|
||||
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');
|
||||
const el = document.querySelector('#asset-card');
|
||||
return !!el && !el.hidden;
|
||||
})()"""))
|
||||
})()"""),
|
||||
page.eval("""(() => { const el = document.querySelector('#asset-card');
|
||||
return !el ? 'missing' : (el.hidden ? 'hidden' : 'shown'); })()"""))
|
||||
|
||||
# And the same toggle, saved, does travel - the surviving path, end to end.
|
||||
set_sop(db_path, {"assets": False})
|
||||
page.goto(base + "/wp-creation-index.html?project=projA")
|
||||
chk("the same toggle, saved this time, reaches the creator", wait_creator(page))
|
||||
settle(1.2)
|
||||
chk("...and Assets is suppressed, from the SOP alone (X4, the surviving path)",
|
||||
page.eval("""(() => {
|
||||
const el = document.querySelector('#asset-card');
|
||||
return !!el && el.hidden;
|
||||
})()"""),
|
||||
page.eval("""(() => { const el = document.querySelector('#asset-card');
|
||||
return !el ? 'missing' : (el.hidden ? 'hidden' : 'shown'); })()"""))
|
||||
set_sop(db_path, {})
|
||||
|
||||
print("\n an older SOP that predates CR-006 gets every section, not none")
|
||||
set_sop_no_sections(db_path)
|
||||
|
||||
@@ -253,10 +253,23 @@ def run(page, base, tok):
|
||||
return focused && document.getElementById('wp-toast').hidden;
|
||||
})()"""))
|
||||
|
||||
page.eval("switchTool('wp'); loadSampleData()")
|
||||
# This used to be "loading the sample on the wrong tab interrupts", because the
|
||||
# wizard's Load sample was context-aware: on the WP tab it reached into the
|
||||
# iframe and called the creator's own loadExample(), and complained when the
|
||||
# frame was not there. B7/T7.1 made the creator a page, so there is no wrong
|
||||
# tab to be on and nothing to reach into - this page's sample is the SOP
|
||||
# sample, always. What is left to check is that the one remaining edge, the
|
||||
# SOP gate, still says something rather than nothing.
|
||||
page.eval("showCreatorGate(); loadSampleData()")
|
||||
settle(1.2)
|
||||
chk("loading the sample on the wrong tab interrupts (role=alert)",
|
||||
page.eval("document.getElementById('wp-toast').getAttribute('role')") == "alert",
|
||||
chk("from the gate, loading the sample announces rather than failing silently",
|
||||
page.eval("!document.getElementById('wp-toast').hidden"))
|
||||
chk("...politely, because being on the gate is not an error",
|
||||
page.eval("document.getElementById('wp-toast').getAttribute('role')") == "status",
|
||||
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
|
||||
chk("...and says where the work package sample actually is",
|
||||
"Work Package Creation" in (page.eval(
|
||||
"(document.getElementById('wp-toast')||{}).textContent||''")),
|
||||
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
|
||||
page.eval("switchTool('sop')")
|
||||
settle(0.6)
|
||||
|
||||
Reference in New Issue
Block a user