11import json
22import shutil
33import subprocess
4+ from contextlib import contextmanager
5+ from functools import partial
6+ from http .server import SimpleHTTPRequestHandler , ThreadingHTTPServer
47from pathlib import Path
5- from typing import Any , cast
8+ from threading import Thread
9+ from typing import Any , Iterator , cast
610
711import numpy as np
812import pytest
@@ -54,7 +58,7 @@ def _chrome_executable() -> str:
5458 pytest .skip ("headless Chrome/Chromium is not available" )
5559
5660
57- def _dump_dom (path : Path ) -> subprocess .CompletedProcess [str ]:
61+ def _dump_dom (url : str ) -> subprocess .CompletedProcess [str ]:
5862 return subprocess .run (
5963 [
6064 _chrome_executable (),
@@ -64,7 +68,7 @@ def _dump_dom(path: Path) -> subprocess.CompletedProcess[str]:
6468 "--enable-logging=stderr" ,
6569 "--log-level=0" ,
6670 "--dump-dom" ,
67- path . resolve (). as_uri () ,
71+ url ,
6872 ],
6973 check = False ,
7074 capture_output = True ,
@@ -73,6 +77,38 @@ def _dump_dom(path: Path) -> subprocess.CompletedProcess[str]:
7377 )
7478
7579
80+ def _rendered_report_html (dom : str ) -> str :
81+ marker = '<div id="rtichoke-report">'
82+ start = dom .index (marker ) + len (marker )
83+ end = dom .index ('<script id="rtichoke-report-spec"' , start )
84+ return dom [start :end ]
85+
86+
87+ @contextmanager
88+ def _serve (directory : Path ) -> Iterator [str ]:
89+ handler = partial (SimpleHTTPRequestHandler , directory = str (directory ))
90+ server = ThreadingHTTPServer (("127.0.0.1" , 0 ), handler )
91+ thread = Thread (target = server .serve_forever , daemon = True )
92+ thread .start ()
93+ try :
94+ yield f"http://127.0.0.1:{ server .server_port } "
95+ finally :
96+ server .shutdown ()
97+ thread .join ()
98+ server .server_close ()
99+
100+
101+ def _assert_report_rendered (browser : subprocess .CompletedProcess [str ]) -> None :
102+ assert browser .returncode == 0 , browser .stderr
103+ rendered = _rendered_report_html (browser .stdout )
104+ assert rendered .strip (), browser .stderr
105+ assert "Performance" in rendered
106+ assert "ROC" in rendered
107+ assert "Calibration" in rendered
108+ assert "<table" in rendered
109+ assert rendered .count ("<svg" ) >= 2
110+
111+
76112def test_default_summary_report_keeps_historical_r_backend (monkeypatch , capsys ):
77113 probs , reals = _inputs ()
78114 calls = []
@@ -159,13 +195,25 @@ def test_browser_summary_report_executes_when_opened_directly(tmp_path):
159195 )
160196 assert isinstance (output , Path )
161197
162- browser = _dump_dom (output )
198+ browser = _dump_dom (output . resolve (). as_uri () )
163199
164- assert browser .returncode == 0 , browser .stderr
165- assert 'id="rtichoke-report"></div>' not in browser .stdout
166- assert "Performance" in browser .stdout
167- assert "ROC" in browser .stdout
168- assert "Calibration" in browser .stdout
200+ _assert_report_rendered (browser )
201+
202+
203+ def test_browser_summary_report_executes_over_localhost (tmp_path ):
204+ probs , reals = _inputs ()
205+ output = create_summary_report (
206+ probs ,
207+ reals ,
208+ renderer = "browser" ,
209+ output_file = tmp_path / "browser_report.html" ,
210+ )
211+ assert isinstance (output , Path )
212+
213+ with _serve (output .parent ) as base_url :
214+ browser = _dump_dom (f"{ base_url } /{ output .name } " )
215+
216+ _assert_report_rendered (browser )
169217
170218
171219def test_browser_summary_report_preserves_component_local_identity (tmp_path ):
0 commit comments