mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
forgetest's page moves onto Bootstrap with forgectrl's theme; forgectrl pinned at the panel overhaul
The acceptance page is assembled by page.py from forgetest/forgetest/ui/ (index.html, page.css, help.js, app.js) plus theme.css and the vendored Bootstrap files, which are byte for byte the ones forgectrl's panel carries, so the two pages look like one product and share the light and dark themes (same localStorage key). A plain file is read in a checkout; on the dev image the recipe installs ui/ gzipped and page.py reads the .gz sibling, inflating once at first request: the rootfs is raw ext4, so bytes in the package are bytes on the image. The explanatory prose (campaign rules, the queues, the campaign actions, the prerequisites switch, the bench intro) is a "?" popover with a link into the documentation site; operator steps, prompts, notices and the live-laser acknowledgment stay in the page, and confirmLive() stays a blocking dialog. The page's own rules hold: rows, prompt buttons and tool entries are built once and updated in place, and the popovers sit on static markup only, so no rebuild orphans one. On a phone the Run pane goes to the top for the duration of a run. scripts/check-ui-vendor.py compares the shared files against forgectrl at its pinned revision (or a local checkout with --forgectrl); it runs in forgetest-ci.yml, so the copies cannot drift. Tests: test_page.py (the gzipped install assembles to the same bytes as a checkout, one self-contained response, the token placeholder once, a missing marker refused); test_server asserts the served page's invariants; test_responsiveness keeps its rules with needles pointed at the new files, its ASCII rule applied to our own sources (Bootstrap's CSS carries an em dash of its own), and its self-contained rule testing asset tags rather than the presence of https:// (the documentation links are meant to be there). forgectrl.panel-serves gains two needles for the panel's theme attribute and save bar. Proof: the unit suite, and the page in Chrome against a fake catalog (both themes, popovers, the bench tab, a full operator run with its prompt, abort). forgectrl pinned at 9d1f6f2 (the panel on Bootstrap, one save bar, help popovers, themes, the gzipped page); PV unchanged. The pin moves only forgectrl's fingerprint. The forgetest changes are the harness's own and have no catalog consequence.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
"""The page assembly: the ui/ files inlined in order, the gzipped install
|
||||
(what the dev image carries) assembling to the same bytes as a checkout,
|
||||
the token placeholder carried exactly once, and a missing marker refused
|
||||
rather than served."""
|
||||
import gzip
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from forgetest import page
|
||||
|
||||
|
||||
def gzipped_copy(src_dir):
|
||||
"""A copy of src_dir with every file gzipped in place, the way the
|
||||
recipe installs ui/."""
|
||||
tmp = tempfile.mkdtemp(prefix="forgetest-ui-")
|
||||
for root, _, files in os.walk(src_dir):
|
||||
rel = os.path.relpath(root, src_dir)
|
||||
dst_dir = os.path.normpath(os.path.join(tmp, rel))
|
||||
os.makedirs(dst_dir, exist_ok=True)
|
||||
for f in files:
|
||||
with open(os.path.join(root, f), "rb") as i, \
|
||||
gzip.open(os.path.join(dst_dir, f + ".gz"), "wb") as o:
|
||||
o.write(i.read())
|
||||
return tmp
|
||||
|
||||
|
||||
class PageTests(unittest.TestCase):
|
||||
def test_checkout_and_gzipped_install_agree(self):
|
||||
plain = page.assemble()
|
||||
tmp = gzipped_copy(page.UI_DIR)
|
||||
try:
|
||||
self.assertFalse(os.path.exists(os.path.join(tmp, "index.html")))
|
||||
self.assertEqual(page.assemble(tmp), plain)
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
def test_page_is_self_contained(self):
|
||||
html = page.assemble()
|
||||
self.assertNotIn("<link ", html)
|
||||
self.assertNotIn("<script src=", html)
|
||||
self.assertEqual(html.count(page.TOKEN_MARK), 1)
|
||||
self.assertIn("data-bs-theme", html)
|
||||
for name in page.CSS_FILES + page.JS_FILES:
|
||||
self.assertNotIn('href="%s"' % name, html)
|
||||
self.assertNotIn('src="%s"' % name, html)
|
||||
|
||||
def test_render_substitutes_the_token(self):
|
||||
out = page.render("deadbeef0123")
|
||||
self.assertEqual(out.count("deadbeef0123"), 1)
|
||||
self.assertNotIn(page.TOKEN_MARK, out)
|
||||
|
||||
def test_missing_marker_is_refused(self):
|
||||
tmp = tempfile.mkdtemp(prefix="forgetest-ui-")
|
||||
try:
|
||||
with open(os.path.join(tmp, "index.html"), "w", encoding="utf-8") as f:
|
||||
f.write("<html><head></head><body>no markers</body></html>")
|
||||
with self.assertRaises(ValueError):
|
||||
page.assemble(tmp)
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -188,16 +188,20 @@ class PageTests(unittest.TestCase):
|
||||
# the queue controls are static markup: a poll relabels them and
|
||||
# flips disabled, it never replaces the node
|
||||
for bid in ("q-unattended", "q-attended", "q-stop"):
|
||||
self.assertIn("id='%s'" % bid, html)
|
||||
self.assertIn('id="%s"' % bid, html)
|
||||
self.assertNotIn("id='" + bid + "-", html)
|
||||
self.assertNotIn('id="' + bid + "-", html)
|
||||
self.assertIn("function renderQueue()", html)
|
||||
# the help popovers sit on static markup only: a rebuilt row or
|
||||
# tool entry would orphan one
|
||||
self.assertNotIn("data-help", html[html.index("function buildGroups("):])
|
||||
|
||||
def test_every_group_is_built_on_the_same_grid(self):
|
||||
"""The subsystems are separate tables. Left to size themselves
|
||||
from their own content no two line up, which is what made the
|
||||
page look busy, so they share one colgroup and a fixed layout."""
|
||||
html = page.render("0" * 32)
|
||||
self.assertIn("#groups table{table-layout:fixed", html)
|
||||
self.assertRegex(html, r"#groups table\s*\{\s*table-layout:\s*fixed")
|
||||
# one colgroup definition, emitted into every group's table
|
||||
self.assertEqual(html.count('var COLS="<colgroup>'), 1)
|
||||
self.assertEqual(html.count('<table>"+COLS+"'), 1,
|
||||
@@ -210,7 +214,7 @@ class PageTests(unittest.TestCase):
|
||||
"a column per header, the action column included")
|
||||
# every fixed column carries a width, or the grid is only a wish
|
||||
for cls in re.findall(r"<col class='(\w)'", colgroup):
|
||||
self.assertRegex(html, r"#groups col\.%s\{width:\d+px\}" % cls)
|
||||
self.assertRegex(html, r"#groups col\.%s\s*\{\s*width:\s*\d+px" % cls)
|
||||
|
||||
def test_details_and_the_requires_note_stay_out_of_the_columns(self):
|
||||
"""Both are long enough to stretch a cell. In a column they would
|
||||
@@ -223,16 +227,24 @@ class PageTests(unittest.TestCase):
|
||||
# message; the action cell carries the button and nothing else
|
||||
self.assertIn("<td><div id='st-\"+d+\"'></div><div id='unmet-\"+d+"
|
||||
"\"'></div><div id='note-\"+d+\"'></div></td>", html)
|
||||
self.assertIn("<td><button class='pri' id='btn-\"+d+\"' "
|
||||
self.assertIn("<td><button class='btn btn-sm btn-primary' id='btn-\"+d+\"' "
|
||||
"onclick='startTest(\\\"\"+d+\"\\\")'>Start</button></td>", html)
|
||||
|
||||
def test_page_is_self_contained_ascii(self):
|
||||
html = page.render("0" * 32)
|
||||
self.assertNotIn("__TOKEN__", html)
|
||||
html.encode("ascii") # no stray typography in an embedded page
|
||||
# our own sources stay ASCII (no stray typography in an embedded
|
||||
# page); the vendored Bootstrap carries its own glyphs
|
||||
for name in ("index.html",) + page.CSS_FILES + page.JS_FILES:
|
||||
if name.startswith("vendor/"):
|
||||
continue
|
||||
page.read_ui(name).encode("ascii")
|
||||
stray = sorted(set(hex(ord(c)) for c in html if ord(c) < 32 and c != "\n"))
|
||||
self.assertEqual(stray, [], "control characters in the page source")
|
||||
for remote in ("http://", "https://", "//cdn"):
|
||||
# nothing fetched from anywhere: the documentation links open a
|
||||
# site, they are not assets
|
||||
for remote in ("<link ", "<script src=", "@import", "url(http", "url(//",
|
||||
'src="http', "src='http", "//cdn"):
|
||||
self.assertNotIn(remote, html)
|
||||
|
||||
|
||||
|
||||
@@ -152,6 +152,13 @@ class ServerTests(unittest.TestCase):
|
||||
st, d = self.call("GET", "/", token=False)
|
||||
self.assertEqual(st, 200)
|
||||
self.assertIn(self.token.encode(), d)
|
||||
page = d.decode("utf-8")
|
||||
# one self-contained response: nothing linked, the token once,
|
||||
# the theme attribute the head script sets
|
||||
self.assertEqual(page.count(self.token), 1)
|
||||
self.assertNotIn("<link ", page)
|
||||
self.assertNotIn("<script src=", page)
|
||||
self.assertIn("data-bs-theme", page)
|
||||
st, d = self.call("GET", "/state", headers={"Host": "evil.example.net"})
|
||||
self.assertEqual(st, 403)
|
||||
st, d = self.call("GET", "/state", headers={"Origin": "http://evil.example.net"})
|
||||
|
||||
Reference in New Issue
Block a user