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>
383 lines
18 KiB
Python
383 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""The launcher's pipeline strip — B4 surface (T5.3).
|
|
|
|
T4.1 put the counts on the server; this is the surface that shows them, and the
|
|
failure it has to avoid is the one B4 names: a per-browser number that looks
|
|
authoritative. So the strip is checked the way aggregates_check.py checks the
|
|
dashboard — by POISONING localStorage with different numbers and demanding the
|
|
strip still report the server's.
|
|
|
|
1. every number comes from a server endpoint
|
|
2. each cell links to a filtered view via a shareable URL, and the filter
|
|
actually applies at the other end
|
|
3. a project with zero work packages renders a sentence, not four zeros
|
|
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 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.
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
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, PW # noqa: E402
|
|
|
|
READY = "!!document.querySelector('#pipeline-strip .pipe-cell, #pipeline-strip .pipe-empty, " \
|
|
"#pipeline-strip .pipe-error')"
|
|
|
|
CELLS_JS = r"""
|
|
JSON.stringify([...document.querySelectorAll('#pipeline-strip .pipe-cell')].map(a => ({
|
|
href: a.getAttribute('href'),
|
|
num: (a.querySelector('.pipe-num') || {}).textContent,
|
|
label: ((a.querySelector('.pipe-label') || {}).textContent || '').trim(),
|
|
sub: ((a.querySelector('.pipe-sub') || {}).textContent || '').trim(),
|
|
accent: getComputedStyle(a).borderLeftColor,
|
|
tag: a.tagName,
|
|
})))
|
|
"""
|
|
|
|
|
|
def settle(seconds=1.4):
|
|
time.sleep(seconds)
|
|
|
|
|
|
# The shape ProjectData.pushSOP writes: { sop, state }. browser_check's fixture
|
|
# stores a bare {governance: …} blob, which is enough for the field view but NOT
|
|
# for the SOP wizard — restoreSavedSOP() needs `state` and bails without it, so
|
|
# the Work Package tab shows its "complete the SOP first" gate and a pipeline link
|
|
# lands on a locked door. Production data has both keys; the fixture should too,
|
|
# or the probe is testing a shape no real project has.
|
|
SOP_DATA = {
|
|
"sop": {"meta": {"tool": "Work Package Configuration", "sample": False},
|
|
"project": {"name": "Job A", "number": "A-1", "client": "Internal QA"},
|
|
"governance": {"disciplines": ["Mechanical", "Electrical"],
|
|
"woFormat": "WP##-[TYPE]"},
|
|
"woTypes": [{"name": "Conduit Install", "enabled": True}]},
|
|
"state": {"bimEnabled": False,
|
|
"project": {"name": "Job A", "number": "A-1", "client": "Internal QA",
|
|
"division": "Internal", "site": "QA Lab"},
|
|
"team": {"pm": "", "apm": "", "cm": "", "qm": ""},
|
|
"teamIds": {"pm": "", "apm": "", "cm": "", "qm": ""},
|
|
"teamMembers": [],
|
|
"signoffRoles": [{"role": "Superintendent", "name": ""},
|
|
{"role": "Foreman", "name": ""}],
|
|
"wpTypes": [{"name": "Conduit Install", "enabled": True, "notes": "",
|
|
"approval": "", "specSection": ""}],
|
|
"governance": {"woformat": "WP##-[TYPE]", "wosize": "", "issuance": [],
|
|
"disciplines": ["Mechanical", "Electrical"],
|
|
"discMode": "choice", "instanceSuffix": "letter",
|
|
"sizeHoursMax": ""},
|
|
"quality": {"qcreq": "Yes", "photo": "", "hold": ""},
|
|
"platforms": {"tracking": "CxAlloy", "commissioning": "CxAlloy",
|
|
"trackingUrl": "", "commissioningUrl": ""},
|
|
"constraints": [], "sequence": [], "sources": []},
|
|
}
|
|
|
|
|
|
def seed_two_projects(db_path):
|
|
"""browser_check's fixture, plus a third project with no work packages at all
|
|
— the empty state is a database state and cannot be faked in the browser."""
|
|
tok = seed(db_path)
|
|
from server.db import SessionLocal
|
|
from server import models
|
|
with SessionLocal() as db:
|
|
sop = db.get(models.Sop, "sopA")
|
|
if sop:
|
|
sop.data = SOP_DATA
|
|
db.add(models.Project(id="projEmpty", name="Empty Job", number="E-1",
|
|
client="Internal QA"))
|
|
db.flush()
|
|
db.add(models.ProjectMember(id="pmE", user_id="user_root", project_id="projEmpty",
|
|
role=""))
|
|
# One package that is genuinely overdue and one on hold, so three of the
|
|
# four cells are non-zero and a strip that hardcoded them would show.
|
|
db.add(models.WorkPackage(
|
|
id="wpA3", project_id="projA", sop_id="sopA", number="WP03-HOLD",
|
|
subject="held for access", status="Issue", type="Conduit Install",
|
|
data={"disciplines": ["Electrical"], "hours": "8",
|
|
"constraints": [{"name": "Access", "status": "open", "comment": "no permit"}]}))
|
|
db.add(models.WorkPackage(
|
|
id="wpA4", project_id="projA", sop_id="sopA", number="WP04-LATE",
|
|
subject="late wire pull", status="In Progress", type="Conduit Install",
|
|
data={"disciplines": ["Electrical"], "hours": "12", "due": "2020-01-01",
|
|
"constraints": []}))
|
|
db.commit()
|
|
return tok
|
|
|
|
|
|
def server_metrics(page, base, project_id):
|
|
return json.loads(page.eval(
|
|
"fetch('/api/wps/metrics?project_id=%s', {headers:{Accept:'application/json'}})"
|
|
".then(r => r.text())" % project_id))
|
|
|
|
|
|
def run(page, base, tok):
|
|
def visit(path, wait=READY):
|
|
page.clear_cookies()
|
|
page.set_cookie("wp_session", tok["root"])
|
|
page.goto(base + path, wait)
|
|
settle(1.6)
|
|
|
|
print("\n1. every number comes from the server")
|
|
visit("/index.html?project=projA")
|
|
chk("the page boots with no JavaScript errors", not page.js_errors(), page.js_errors())
|
|
truth = server_metrics(page, base, "projA")
|
|
cells = json.loads(page.eval(CELLS_JS))
|
|
chk("the strip renders four cells", len(cells) == 4, [c["label"] for c in cells])
|
|
want = [("Work packages", truth["total"]), ("Release ready", truth["release_ready"]),
|
|
("On hold", truth["on_hold"]), ("Overdue", truth["overdue"])]
|
|
for (label, n), cell in zip(want, cells):
|
|
chk("%-14s reads %s, the server's own number" % (label, n),
|
|
cell["label"] == label and cell["num"] == str(n),
|
|
"cell %r = %r, server = %r" % (cell["label"], cell["num"], n))
|
|
chk("...and the fixture is not all zeros, so this proves something",
|
|
truth["total"] > 0 and truth["on_hold"] > 0 and truth["overdue"] > 0, truth)
|
|
|
|
print("\n1b. a poisoned cache does not move a single number")
|
|
page.eval("""(() => {
|
|
// Every shape the launcher has ever cached work packages under, filled with
|
|
// numbers nothing like the server's.
|
|
const fake = Array.from({length: 99}, (_, i) => ({id: 'x' + i, status: 'Closed'}));
|
|
for (const k of ['wp_packages', 'wp_packages__projA', 'wp_suite_wps',
|
|
'wp_suite_wps__projA', 'wp_creation_packages']) {
|
|
localStorage.setItem(k, JSON.stringify(fake));
|
|
}
|
|
return true;
|
|
})()""")
|
|
page.goto(base + "/index.html?project=projA", READY)
|
|
settle(1.6)
|
|
poisoned = json.loads(page.eval(CELLS_JS))
|
|
chk("with 99 fake packages in localStorage the total is still the server's %d"
|
|
% truth["total"],
|
|
poisoned and poisoned[0]["num"] == str(truth["total"]),
|
|
poisoned[0]["num"] if poisoned else poisoned)
|
|
chk("...and no cell reads 99", all(c["num"] != "99" for c in poisoned),
|
|
[c["num"] for c in poisoned])
|
|
page.eval("localStorage.clear(); true")
|
|
|
|
print("\n4. the strip announces, and says when it is working")
|
|
chk("the strip is a live region", page.eval(
|
|
"document.getElementById('pipeline-strip').getAttribute('aria-live')") == "polite")
|
|
chk("...polite, not assertive — a count is not an interruption", page.eval(
|
|
"document.getElementById('pipeline-strip').getAttribute('aria-live')") != "assertive")
|
|
chk("...and reports aria-busy=false once the numbers land", page.eval(
|
|
"document.getElementById('pipeline-strip').getAttribute('aria-busy')") == "false")
|
|
chk("the section is named", page.eval(
|
|
"!!document.querySelector('#pipeline[aria-labelledby]')"))
|
|
|
|
print("\n the four cells are told apart by more than a colour (C1)")
|
|
chk("every cell carries a label in words",
|
|
all(c["label"] for c in poisoned), [c["label"] for c in poisoned])
|
|
chk("...and a sentence saying what it counts",
|
|
all(len(c["sub"]) > 8 for c in poisoned), [c["sub"] for c in poisoned])
|
|
chk("...four distinct labels", len({c["label"] for c in poisoned}) == 4)
|
|
chk("...and four distinct accents, as a second channel",
|
|
len({c["accent"] for c in poisoned}) == 4, [c["accent"] for c in poisoned])
|
|
|
|
print("\n2. each cell is a shareable link into a filtered board")
|
|
chk("every cell is a real <a>, not a div with a handler",
|
|
all(c["tag"] == "A" for c in poisoned), [c["tag"] for c in poisoned])
|
|
hrefs = [c["href"] for c in poisoned]
|
|
chk("...all pointing at the dashboard",
|
|
all("view=dashboard" in h for h in hrefs), hrefs)
|
|
chk("...all carrying the project, so the link works on a cold browser",
|
|
all("project=projA" in h for h in hrefs), hrefs)
|
|
chk("...three of them naming a filter, the total naming none",
|
|
[("flag=" in h) for h in hrefs] == [False, True, True, True], hrefs)
|
|
chk("...and the filters are the ones the dashboard knows",
|
|
sorted(h.split("flag=")[1] for h in hrefs if "flag=" in h)
|
|
== ["onhold", "overdue", "ready"], hrefs)
|
|
|
|
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):
|
|
if page.eval("!!window.wpCreatorReady"):
|
|
break
|
|
time.sleep(0.3)
|
|
settle(2.0)
|
|
# 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 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 = [...document.querySelectorAll('.dash-panel')].find(p => {
|
|
const t = p.querySelector('.dash-panel-title');
|
|
return t && /^Work packages/.test(t.textContent.trim());
|
|
});
|
|
const rows = panel
|
|
? [...panel.querySelectorAll('tbody tr')]
|
|
.map(r => (r.cells[0] ? r.cells[0].textContent : '').trim()).filter(Boolean)
|
|
: null;
|
|
return JSON.stringify({
|
|
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",
|
|
(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")
|
|
chk("no cells are rendered", page.eval(
|
|
"document.querySelectorAll('#pipeline-strip .pipe-cell').length") == 0,
|
|
page.eval("document.querySelectorAll('#pipeline-strip .pipe-cell').length"))
|
|
empty_txt = page.eval("(document.querySelector('#pipeline-strip .pipe-empty')||{}).textContent||''")
|
|
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]))
|
|
# 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)
|
|
|
|
print("\n5. a failed request is an error, not a zero and not a memory")
|
|
visit("/index.html?project=projA")
|
|
before = json.loads(page.eval(CELLS_JS))
|
|
chk("the strip has real numbers to lose", len(before) == 4)
|
|
page.eval("""(() => {
|
|
const real = window.fetch;
|
|
window.fetch = function (u, o) {
|
|
if (String(u).indexOf('/api/wps/metrics') >= 0) return Promise.reject(new Error('probe offline'));
|
|
return real.call(this, u, o);
|
|
};
|
|
renderPipeline(ProjectData.getActive());
|
|
return true;
|
|
})()""")
|
|
for _ in range(20):
|
|
if page.eval("!!document.querySelector('#pipeline-strip .pipe-error')"):
|
|
break
|
|
time.sleep(0.3)
|
|
settle(0.6)
|
|
err = page.eval("(document.querySelector('#pipeline-strip .pipe-error')||{}).textContent||''")
|
|
chk("an explicit error is shown", bool(err.strip()), repr(err))
|
|
chk("...naming the failure", "probe offline" in err, repr(err.strip()[:90]))
|
|
chk("...with no cells left showing the numbers from before", page.eval(
|
|
"document.querySelectorAll('#pipeline-strip .pipe-cell').length") == 0,
|
|
page.eval("document.querySelectorAll('#pipeline-strip .pipe-cell').length"))
|
|
chk("...and no zeros in their place", page.eval(
|
|
"document.querySelectorAll('#pipeline-strip .pipe-num').length") == 0)
|
|
|
|
print("\n both widths")
|
|
for w, label in ((390, "390px"), (1440, "1440px")):
|
|
page.viewport(w, 900, mobile=(w == 390))
|
|
visit("/index.html?project=projA")
|
|
chk("%s: four cells still render" % label, page.eval(
|
|
"document.querySelectorAll('#pipeline-strip .pipe-cell').length") == 4)
|
|
chk("%s: the page does not scroll sideways" % label,
|
|
page.eval("document.documentElement.scrollWidth <= window.innerWidth + 1"),
|
|
page.eval("[document.documentElement.scrollWidth, window.innerWidth]"))
|
|
chk("%s: every cell is at least a 44px tap target" % label,
|
|
page.eval("[...document.querySelectorAll('#pipeline-strip .pipe-cell')]"
|
|
".every(a => a.getBoundingClientRect().height >= 44)"),
|
|
page.eval("[...document.querySelectorAll('#pipeline-strip .pipe-cell')]"
|
|
".map(a => Math.round(a.getBoundingClientRect().height))"))
|
|
page.viewport(1400, 1000)
|
|
|
|
print("\n no localStorage read sits behind any of these numbers")
|
|
src = open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"html", "index.html"), encoding="utf-8").read()
|
|
start = src.find("function renderPipeline")
|
|
body = src[start:src.find("\n }", start)] if start >= 0 else ""
|
|
chk("renderPipeline touches no localStorage", start >= 0 and "localStorage" not in body,
|
|
body[:200])
|
|
chk("...and reads its numbers from /api/wps/metrics", "/api/wps/metrics" in body)
|
|
|
|
|
|
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-pipeline-")
|
|
db_path = os.path.join(tmpdir, "check.db")
|
|
server = None
|
|
try:
|
|
tok = seed_two_projects(db_path)
|
|
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("\nLauncher pipeline strip — B4 surface\nTarget: %s" % base)
|
|
|
|
browser = cdp.Browser(exe)
|
|
page = browser.page()
|
|
try:
|
|
run(page, base, tok)
|
|
finally:
|
|
page.close()
|
|
browser.close()
|
|
finally:
|
|
if server:
|
|
server.kill()
|
|
try:
|
|
server.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
pass
|
|
try:
|
|
from server.db import engine
|
|
engine.dispose()
|
|
except Exception:
|
|
pass
|
|
import shutil
|
|
for _ in range(10):
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
if not os.path.exists(tmpdir):
|
|
break
|
|
time.sleep(0.3)
|
|
|
|
total = len(_PASS) + len(_FAIL)
|
|
print("\n%s\n%d/%d checks passed." % ("-" * 54, len(_PASS), total))
|
|
if _FAIL:
|
|
for f in _FAIL:
|
|
print(" - " + f)
|
|
return 1
|
|
print("\nResult: " + _c("ALL PASS — four server counts, four shareable links.", "32") + "\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|