diff options
Diffstat (limited to 'packages/meshbay-hub/tests/harness')
| -rw-r--r-- | packages/meshbay-hub/tests/harness/layout_probe.py | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/harness/layout_probe.py b/packages/meshbay-hub/tests/harness/layout_probe.py new file mode 100644 index 0000000..530b3f0 --- /dev/null +++ b/packages/meshbay-hub/tests/harness/layout_probe.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +Measure a piece of the SPA at a phone width, in a real browser. + +The responsive tests up to now pinned numbers out of the stylesheet, with a +docstring admitting that a layout cannot be measured because there is no +browser in the suite. There is one: Chrome is what the video work has been +verified against. Reading `width: 330px` out of a rule says nothing about +whether the thing lands on the screen — that depends on where its anchor sits, +which depends on everything to its right. + +Renders the real style.css with a fragment of markup, at a given viewport, and +reports the bounding box of each selector asked for. + + layout_probe.py <widths,comma,separated> <html-fragment-file> <selector> [...] + +One browser for all the widths asked for: an iframe apiece, measured in a +single pass. Launching Chrome per width put three minutes on the test suite. +""" +import http.server +import json +import socketserver +import subprocess +import sys +import tempfile +import threading +import time +from pathlib import Path + +STATIC = Path(__file__).resolve().parents[2] / "src" / "meshbay_hub" / "static" +PORT = 8734 + +PAGE = """<!doctype html><html><head><meta charset=utf-8></head> +<body style="margin:0"> +<!-- One iframe per width. A headless window will not go below about 500 px, + and an iframe establishes its own viewport, so media queries inside it see + the phone width we mean. --> +<div id="frames"></div> +<script> +const WIDTHS = %(widths)s, SELECTORS = %(selectors)s; +const FRAG = %(fragment)s; +const host = document.getElementById('frames'); +for (const w of WIDTHS) { + const f = document.createElement('iframe'); + f.id = 'f' + w; + f.style.cssText = `width:${w}px;height:740px;border:0;display:block`; + host.appendChild(f); + const d = f.contentDocument; + d.open(); + d.write(`<!doctype html><html><head><meta charset=utf-8> +<link rel="stylesheet" href="/style.css"></head><body>${FRAG}</body></html>`); + d.close(); +} +setTimeout(() => { + const out = {}; + for (const w of WIDTHS) { + const win = document.getElementById('f' + w).contentWindow; + const r = {viewport: {w: win.innerWidth, h: win.innerHeight}, + docScrollW: win.document.documentElement.scrollWidth, boxes: {}}; + for (const sel of SELECTORS) { + const el = win.document.querySelector(sel); + if (!el) { r.boxes[sel] = null; continue; } + const b = el.getBoundingClientRect(); + r.boxes[sel] = { + left: Math.round(b.left), right: Math.round(b.right), + top: Math.round(b.top), width: Math.round(b.width), + height: Math.round(b.height), + offLeft: Math.round(Math.max(0, -b.left)), + offRight: Math.round(Math.max(0, b.right - win.innerWidth)), + }; + } + out[w] = r; + } + fetch('/log', {method: 'POST', body: JSON.stringify(out)}); +}, 500); +</script></body></html>""" + +RECORDS = [] + + +def main() -> int: + widths = [int(w) for w in sys.argv[1].split(",")] + fragment = Path(sys.argv[2]).read_text() + selectors = sys.argv[3:] + + class H(http.server.BaseHTTPRequestHandler): + def log_message(self, *a): + pass + + def do_POST(self): + RECORDS.append(json.loads( + self.rfile.read(int(self.headers["Content-Length"])).decode())) + self.send_response(204) + self.end_headers() + + def do_GET(self): + if self.path == "/": + body = (PAGE % {"fragment": json.dumps(fragment), + "widths": json.dumps(widths), + "selectors": json.dumps(selectors)}).encode() + ctype = "text/html; charset=utf-8" + elif self.path == "/style.css": + body = (STATIC / "style.css").read_bytes() + ctype = "text/css" + else: + self.send_response(404) + self.end_headers() + return + self.send_response(200) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + class S(socketserver.ThreadingTCPServer): + allow_reuse_address = True + daemon_threads = True + + srv = S(("127.0.0.1", PORT), H) + threading.Thread(target=srv.serve_forever, daemon=True).start() + chrome = subprocess.Popen([ + "google-chrome", "--headless=new", "--no-sandbox", + "--window-size=1000,900", + "--user-data-dir=" + tempfile.mkdtemp(prefix="chrome-layout-"), + f"http://127.0.0.1:{PORT}/", + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + deadline = time.time() + 45 + while time.time() < deadline and not RECORDS: + time.sleep(0.2) + chrome.terminate() + srv.shutdown() + if not RECORDS: + print(json.dumps({"error": "no measurement"})) + return 1 + print(json.dumps(RECORDS[0])) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) |