aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/harness/lazy_failure_probe.py151
-rw-r--r--packages/meshbay-hub/tests/test_lazy_load_failure.py60
2 files changed, 211 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/harness/lazy_failure_probe.py b/packages/meshbay-hub/tests/harness/lazy_failure_probe.py
new file mode 100644
index 0000000..357529a
--- /dev/null
+++ b/packages/meshbay-hub/tests/harness/lazy_failure_probe.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+"""
+What `lazy()` shows when the module it asks for answers 404.
+
+The shape found live: a tab opened before a hub deploy asks for its not-yet-
+loaded modules under the previous `/a/<hash>/` prefix, which the new hub no
+longer serves. Every lazily loaded view — Search, Videos, Music, Photos, the
+video player — sat on its spinner for good, with an empty console. This
+renders the shipped `lazy.js` against a module that does not exist, and one
+that does, and reads back what is on the page.
+
+ lazy_failure_probe.py
+"""
+
+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 = 8763
+RECORDS = []
+socketserver.TCPServer.allow_reuse_address = True
+
+FRAME = r"""<!doctype html><html><head><meta charset=utf-8>
+<link rel="stylesheet" href="/style.css"></head><body>
+<div id="plain"></div><div id="framed"></div><div id="fine"></div>
+<script type="module">
+import { html, render } from '/vendor/htm-preact.js';
+import { initLocale } from '/i18n.js';
+import { lazy } from '/lazy.js';
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+const errors = [];
+const origError = console.error;
+console.error = (...a) => { errors.push(a.map(String).join(' ')); origError(...a); };
+
+const read = (id) => {
+ const root = document.getElementById(id);
+ return {
+ spinner: !!root.querySelector('.spinner'),
+ notice: (root.querySelector('.lazy-failed p') || {}).textContent || null,
+ buttons: [...root.querySelectorAll('.lazy-failed button')].map((b) => b.textContent.trim()),
+ overlay: !!root.querySelector('.video-player-overlay .lazy-failed'),
+ text: root.textContent.trim(),
+ };
+};
+
+(async () => {
+ try {
+ await initLocale();
+ // The stale tab's request: a module under a prefix nobody serves.
+ const Gone = lazy(() => import('/a/0000000000/gone.js'), 'Gone');
+ const frame = (c) => html`<div class="video-overlay video-player-overlay">${c}</div>`;
+ const GoneOverlay = lazy(() => import('/a/0000000000/player.js'), 'Player',
+ frame(html`<p class="page-message"><span class="spinner"></span></p>`), frame);
+ // A module that exists still renders as itself.
+ const Fine = lazy(() => import('/icon.js'), 'Icon');
+
+ let closed = 0;
+ render(html`<${Gone} />`, document.getElementById('plain'));
+ render(html`<${GoneOverlay} onClose=${() => { closed += 1; }} />`,
+ document.getElementById('framed'));
+ render(html`<${Fine} name="close" />`, document.getElementById('fine'));
+ await sleep(1500);
+
+ const out = { plain: read('plain'), framed: read('framed'), errors,
+ fine: !!document.querySelector('#fine svg, #fine .icon') };
+ const btns = document.querySelectorAll('#framed .lazy-failed button');
+ if (btns[1]) btns[1].click();
+ out.closed = closed;
+ parent.postMessage(out, '*');
+ } catch (err) {
+ parent.postMessage({ error: String((err && err.stack) || err) }, '*');
+ }
+})();
+</script></body></html>"""
+
+PAGE = r"""<!doctype html><html><head><meta charset=utf-8></head>
+<body style="margin:0"><iframe src="/case" style="width:900px;height:700px;border:0"></iframe>
+<script>
+addEventListener('message', (e) => fetch('/log', { method: 'POST', body: JSON.stringify(e.data) }));
+</script></body></html>"""
+
+
+class H(http.server.BaseHTTPRequestHandler):
+ def log_message(self, *a):
+ pass
+
+ def do_POST(self):
+ length = int(self.headers.get("Content-Length") or 0)
+ if self.path == "/log":
+ RECORDS.append(json.loads(self.rfile.read(length).decode()))
+ else:
+ self.rfile.read(length)
+ self.send_response(204)
+ self.end_headers()
+
+ def _send(self, body: bytes, ctype: str) -> None:
+ 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)
+
+ def do_GET(self):
+ path = self.path.split("?")[0]
+ if path == "/":
+ self._send(PAGE.encode(), "text/html; charset=utf-8")
+ elif path == "/case":
+ self._send(FRAME.encode(), "text/html; charset=utf-8")
+ else:
+ asset = (STATIC / path.lstrip("/")).resolve()
+ if not str(asset).startswith(str(STATIC)) or not asset.is_file():
+ self.send_response(404)
+ self.end_headers()
+ return
+ self._send(asset.read_bytes(),
+ "text/css" if asset.suffix == ".css"
+ else "text/javascript" if asset.suffix == ".js"
+ else "application/octet-stream")
+
+
+def main() -> int:
+ with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as profile:
+ proc = subprocess.Popen(
+ ["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox",
+ f"--user-data-dir={profile}", "--window-size=900,700",
+ f"http://127.0.0.1:{PORT}/"],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ deadline = time.time() + 60
+ while not RECORDS and time.time() < deadline:
+ time.sleep(0.2)
+ proc.terminate()
+ proc.wait(timeout=20)
+ if not RECORDS:
+ print("the page never reported", file=sys.stderr)
+ return 1
+ print(json.dumps(RECORDS[0]))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/packages/meshbay-hub/tests/test_lazy_load_failure.py b/packages/meshbay-hub/tests/test_lazy_load_failure.py
new file mode 100644
index 0000000..00c2030
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_lazy_load_failure.py
@@ -0,0 +1,60 @@
+"""
+A view whose module cannot be fetched says so, instead of spinning for good.
+
+Found live after a hub deploy: the hub serves the module graph under
+`/a/<hash>/` for the current hash only, so a tab opened before the deploy asked
+for Search, Videos, Music, Photos and the player under a prefix that now
+answers 404. `lazy.js` swallowed the rejection and kept the placeholder, so
+every one of them showed a spinner for ever, with an empty console, while
+Files and Chat — loaded before the deploy — worked. A reload fixed it, and
+nothing on screen said so.
+
+Measured in a browser: a rejected dynamic import is the one thing no source
+reading can produce.
+"""
+import json
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+HARNESS = Path(__file__).parent / "harness" / "lazy_failure_probe.py"
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+
+pytestmark = pytest.mark.skipif(
+ shutil.which("google-chrome") is None or not (STATIC / "lazy.js").exists(),
+ reason="Chrome or the SPA sources are not available")
+
+
+@pytest.fixture(scope="module")
+def probe():
+ run = subprocess.run(["python3", str(HARNESS)], capture_output=True, timeout=120)
+ assert run.returncode == 0, run.stderr.decode()[-2000:]
+ out = json.loads(run.stdout.decode())
+ assert "error" not in out, out["error"]
+ return out
+
+
+@pytest.mark.parametrize("view", ["plain", "framed"])
+def test_a_module_that_answers_404_is_not_a_spinner(probe, view):
+ assert not probe[view]["spinner"], "still spinning over a module that will never come"
+ assert probe[view]["notice"], "nothing on screen says the view failed to load"
+ assert probe[view]["buttons"], "no way offered to reload"
+
+
+def test_the_failure_reaches_the_console(probe):
+ """An empty console is what made this cost a scare instead of a glance."""
+ assert len(probe["errors"]) == 2
+ assert all("could not load" in e for e in probe["errors"])
+
+
+def test_an_overlay_fails_inside_its_overlay_and_can_be_closed(probe):
+ assert probe["framed"]["overlay"], "the player's notice landed outside its overlay"
+ assert len(probe["framed"]["buttons"]) == 2
+ assert probe["closed"] == 1, "the Close button did not reach the caller's onClose"
+ assert len(probe["plain"]["buttons"]) == 1, "no onClose, so no Close button"
+
+
+def test_a_module_that_exists_still_renders(probe):
+ assert probe["fine"]