aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/harness')
-rw-r--r--packages/meshbay-hub/tests/harness/layout_probe.py140
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())