Localhost port watcher: detects a newly bound dev-server port via bounded TCP probing, opens an iframe preview pane, and captures a one-click timestamped screenshot receipt via headless Chrome. No Claude Code changes, stdlib-only, 16 passing tests plus a real end-to-end dogfood (demo.py) with a genuine recorded screenshot. Landing page at ventures/preview-sidecar/site/index.html uses a receipt/ticket visual identity (narrow thermal-paper column, dotted rules, wax stamp, monospace totals) distinct from the finishline venture, with one above-the-fold signup CTA and an honest free/paid pricing section. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
114 lines
4.1 KiB
Python
114 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
import socket
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import sidecar
|
|
|
|
|
|
class ParsePorts(unittest.TestCase):
|
|
def test_default_when_nothing_given(self):
|
|
self.assertEqual(sidecar.parse_ports(None, None), sorted(sidecar.DEFAULT_PORTS))
|
|
|
|
def test_explicit_list(self):
|
|
self.assertEqual(sidecar.parse_ports("3000,3000,5173", None), [3000, 5173])
|
|
|
|
def test_range(self):
|
|
self.assertEqual(sidecar.parse_ports(None, "3000-3003"), [3000, 3001, 3002, 3003])
|
|
|
|
def test_range_too_wide_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
sidecar.parse_ports(None, f"1-{sidecar.MAX_PORTS + 50}")
|
|
|
|
def test_bad_port_rejected(self):
|
|
with self.assertRaises(ValueError):
|
|
sidecar.parse_ports("70000", None)
|
|
|
|
|
|
class ProbeAndDiff(unittest.TestCase):
|
|
def test_probe_detects_a_real_listener(self):
|
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
srv.bind(("127.0.0.1", 0))
|
|
srv.listen(1)
|
|
port = srv.getsockname()[1]
|
|
try:
|
|
self.assertTrue(sidecar.probe_port("127.0.0.1", port))
|
|
finally:
|
|
srv.close()
|
|
|
|
def test_probe_false_for_closed_port(self):
|
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
srv.bind(("127.0.0.1", 0))
|
|
port = srv.getsockname()[1]
|
|
srv.close()
|
|
self.assertFalse(sidecar.probe_port("127.0.0.1", port, timeout=0.2))
|
|
|
|
def test_diff_new_only_reports_additions(self):
|
|
self.assertEqual(sidecar.diff_new({3000}, {3000, 5173}), {5173})
|
|
self.assertEqual(sidecar.diff_new({3000, 5173}, {5173}), set())
|
|
|
|
|
|
class PreviewHtml(unittest.TestCase):
|
|
def test_render_contains_iframe_and_url(self):
|
|
html = sidecar.render_preview_html("127.0.0.1", 5173, "2026-01-01T00:00:00Z")
|
|
self.assertIn("http://127.0.0.1:5173", html)
|
|
self.assertIn("<iframe", html)
|
|
self.assertIn("open in new tab", html)
|
|
|
|
def test_write_preview_creates_file(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
path, detected_at = sidecar.write_preview(tmp, "127.0.0.1", 4200)
|
|
self.assertTrue(path.exists())
|
|
self.assertIn("4200", path.name)
|
|
self.assertIn("http://127.0.0.1:4200", path.read_text())
|
|
self.assertRegex(detected_at, r"^\d{4}-\d{2}-\d{2}T")
|
|
|
|
|
|
class FindChrome(unittest.TestCase):
|
|
def test_explicit_path_wins_when_present(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
fake = Path(tmp) / "chrome-bin"
|
|
fake.write_text("")
|
|
self.assertEqual(sidecar.find_chrome(str(fake)), str(fake))
|
|
|
|
def test_explicit_missing_path_returns_none(self):
|
|
self.assertIsNone(sidecar.find_chrome("/definitely/not/a/real/chrome/binary"))
|
|
|
|
def test_env_var_used_when_set(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
fake = Path(tmp) / "chrome-env"
|
|
fake.write_text("")
|
|
with mock.patch.dict("os.environ", {"CHROME_PATH": str(fake)}):
|
|
self.assertEqual(sidecar.find_chrome(), str(fake))
|
|
|
|
def test_none_when_nothing_found(self):
|
|
with mock.patch.object(sidecar, "CHROME_CANDIDATES", ["/nope/not/here"]), \
|
|
mock.patch.dict("os.environ", {}, clear=True):
|
|
self.assertIsNone(sidecar.find_chrome())
|
|
|
|
|
|
class MakeReceiptErrors(unittest.TestCase):
|
|
def test_raises_when_port_not_open(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
with self.assertRaises(RuntimeError):
|
|
sidecar.make_receipt("127.0.0.1", 1, tmp, None, 5)
|
|
|
|
def test_raises_when_no_chrome_found(self):
|
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
srv.bind(("127.0.0.1", 0))
|
|
srv.listen(1)
|
|
port = srv.getsockname()[1]
|
|
try:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
with mock.patch.object(sidecar, "find_chrome", return_value=None):
|
|
with self.assertRaises(RuntimeError):
|
|
sidecar.make_receipt("127.0.0.1", port, tmp, None, 5)
|
|
finally:
|
|
srv.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|