| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- """The mobile-layout bundle (goal 41 T12): the server-side invariants
- of the mobile containment contract (docs/spry-conversion.md), plus —
- when chromium is present — the full browser audit (test/mobile_audit)
- that renders every route at 360/768/1280 and fails on any horizontal
- overflow or dialog-fit failure.
- CI without chromium: the browser part skips gracefully (print + pass
- a SKIP marker, never a FAIL) unless T12MOBILE_BROWSER=1 demands it."""
- import os
- import re
- import sys
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- from harness import * # noqa: F401,F403
- WIDE_TABLE_RE = re.compile(r"<table[^>]*>", re.IGNORECASE)
- # Every page that ships a <table> in its compiled markup; the audit's
- # route set mirrors this list.
- PAGES_WITH_TABLES = ("/tasks", "/goals", "/projects", "/chats",
- "/saddles", "/users")
- def run(ctx):
- admin = ctx.auth
- print("== every wide table sits in .table-wrap ==")
- for uri in PAGES_WITH_TABLES:
- page = http_get_page(admin, uri)
- tables = WIDE_TABLE_RE.findall(page)
- check(bool(tables), "%s renders at least one table" % uri)
- for match in WIDE_TABLE_RE.finditer(page):
- before = page[:match.start()]
- # the NEAREST enclosing div before the table must be the
- # wrap (allow attributes between class names)
- opening = before.rfind("<div")
- enclosing = before[opening:match.start()] if opening != -1 else ""
- check("table-wrap" in enclosing,
- "%s: table inside .table-wrap" % uri)
- print("== no fixed pixel widths above the phone viewport ==")
- # Any width/min-width style attribute or CSS in the page markup
- # above 360px is a phone-overflow factory; the audit found them in
- # dialogs and hero forms.
- for uri in ("/", "/tasks", "/goals", "/projects", "/rules",
- "/stacks", "/discoveries", "/chats", "/saddles",
- "/users", "/models", "/profile",
- "/tasks/2", "/goals/1", "/projects/2"):
- page = http_get_page(admin, uri)
- offenders = re.findall(
- r'(?:min-width|width)\s*[:=]\s*"?(\d{4,})', page)
- check(not offenders,
- "%s: no fixed width >= 1000px in markup" % uri)
- print("== dialogs use the fitting width grammar ==")
- # spry's dialog rule is width: min(92vw, <cap>rem); the app never
- # overrides it with a fixed width. Assert the dialog markup keeps
- # the shared classes and app.css carries no fixed dialog width.
- status, css = http("GET", "/_statum/resource/app.css")
- check(status == 200, "app.css served")
- fixed = [line for line in css.splitlines()
- if "dialog" in line and re.search(r"width:\s*\d", line)]
- check(not fixed, "app.css sets no fixed dialog width: %s" % fixed[:2])
- for uri in ("/tasks", "/goals", "/rules", "/projects"):
- page = http_get_page(admin, uri)
- for dialog_id in re.findall(r'<dialog[^>]*id="([^"]+)"', page):
- check(bool(dialog_id), "dialog %s has an id" % dialog_id)
- check('class="dialog-sm"' in page or "<dialog" not in page,
- "%s dialogs carry the .dialog-sm width grammar" % uri)
- print("== the touch-target rules exist in the served css ==")
- check("@media (any-pointer: coarse) and (max-width: 640px)" in css,
- "the phone touch-target media block is served")
- check(re.search(r"\.nav \.nav-toggle[^}]*min-height:\s*44px", css)
- is not None,
- "nav toggles meet the 44px floor on phones")
- print("== browser audit (rendered layout at 360/768/1280) ==")
- chromium = None
- for name in ("chromium-browser", "chromium", "google-chrome",
- "google-chrome-stable"):
- import shutil
- chromium = chromium or shutil.which(name)
- if not chromium and not os.environ.get("T12MOBILE_BROWSER"):
- print("SKIP no chromium binary — the browser audit is skipped"
- " (set T12MOBILE_BROWSER=1 to require it)")
- return
- import mobile_audit
- findings = mobile_audit.run_audit(
- os.environ.get("T12MOBILE_APP") or APP, (360, 768, 1280))
- check(findings is not None, "the browser audit ran")
- if findings is None:
- return
- overflows = []
- dialogs_bad = []
- for route in findings:
- for width in findings[route]:
- data = findings[route][width]
- if data.get("error"):
- overflows.append((route, width, data["error"]))
- continue
- for offender in data.get("offenders") or []:
- overflows.append((route, width, offender["path"]))
- for dialog in data.get("dialogs") or []:
- if not dialog.get("fits", True):
- dialogs_bad.append((route, width, dialog))
- check(not overflows,
- "zero horizontal overflow at 360/768/1280 (%d findings: %s)"
- % (len(overflows), overflows[:3]))
- check(not dialogs_bad,
- "every dialog fits the viewport at phone width (%s)"
- % dialogs_bad[:2])
|