11import json
2+ import shutil
3+ import subprocess
4+ from contextlib import contextmanager
5+ from functools import partial
6+ from http .server import SimpleHTTPRequestHandler , ThreadingHTTPServer
27from pathlib import Path
3- from typing import Any , cast
8+ from threading import Thread
9+ from typing import Any , Iterator , cast
410
511import numpy as np
12+ import pytest
613
714from rtichoke .summary_report import summary_report as summary_report_module
815from rtichoke .summary_report .summary_report import create_summary_report
@@ -38,6 +45,70 @@ def _embedded_report(html: str) -> dict[str, Any]:
3845 return cast (dict [str , Any ], json .loads (html [start :end ]))
3946
4047
48+ def _chrome_executable () -> str :
49+ for candidate in (
50+ "google-chrome" ,
51+ "google-chrome-stable" ,
52+ "chromium" ,
53+ "chromium-browser" ,
54+ ):
55+ executable = shutil .which (candidate )
56+ if executable is not None :
57+ return executable
58+ pytest .skip ("headless Chrome/Chromium is not available" )
59+
60+
61+ def _dump_dom (url : str ) -> subprocess .CompletedProcess [str ]:
62+ return subprocess .run (
63+ [
64+ _chrome_executable (),
65+ "--headless=new" ,
66+ "--no-sandbox" ,
67+ "--disable-gpu" ,
68+ "--enable-logging=stderr" ,
69+ "--log-level=0" ,
70+ "--dump-dom" ,
71+ url ,
72+ ],
73+ check = False ,
74+ capture_output = True ,
75+ text = True ,
76+ timeout = 30 ,
77+ )
78+
79+
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+ assert "INFO:CONSOLE" not in browser .stderr , browser .stderr
104+ rendered = _rendered_report_html (browser .stdout )
105+ assert "Performance" in rendered , browser .stderr
106+ assert "ROC" in rendered
107+ assert "Calibration" in rendered
108+ assert "<table" in rendered
109+ assert rendered .count ("<svg" ) >= 2
110+
111+
41112def test_default_summary_report_keeps_historical_r_backend (monkeypatch , capsys ):
42113 probs , reals = _inputs ()
43114 calls = []
@@ -90,7 +161,8 @@ def test_browser_summary_report_is_opt_in_and_uses_real_canonical_components(
90161
91162 assert result == output
92163 assert output .exists ()
93- assert (tmp_path / "rtichoke-viz.js" ).exists ()
164+ viz_js_path = tmp_path / "rtichoke-viz.js"
165+ assert viz_js_path .exists ()
94166 assert (tmp_path / "rtichoke-viz.css" ).exists ()
95167
96168 html = output .read_text (encoding = "utf-8" )
@@ -107,11 +179,40 @@ def test_browser_summary_report_is_opt_in_and_uses_real_canonical_components(
107179 ]
108180 assert report ["components" ][1 ]["spec" ]["schemaVersion" ] == "2.0"
109181 assert report ["components" ][2 ]["spec" ]["schemaVersion" ] == "2.0"
110- assert 'import { renderReport } from "./rtichoke-viz.js";' in html
182+ assert 'import { renderReport } from "./rtichoke-viz.js";' not in html
183+ assert viz_js_path .read_text (encoding = "utf-8" ) in html
111184 assert "append(renderReport(spec))" in html
112- assert "renderPerformanceTable" not in html
113- assert "renderRocV2" not in html
114- assert "renderCalibrationV2" not in html
185+
186+
187+ def test_browser_summary_report_executes_when_opened_directly (tmp_path ):
188+ probs , reals = _inputs ()
189+ output = create_summary_report (
190+ probs ,
191+ reals ,
192+ renderer = "browser" ,
193+ output_file = tmp_path / "browser_report.html" ,
194+ )
195+ assert isinstance (output , Path )
196+
197+ browser = _dump_dom (output .resolve ().as_uri ())
198+
199+ _assert_report_rendered (browser )
200+
201+
202+ def test_browser_summary_report_executes_over_localhost (tmp_path ):
203+ probs , reals = _inputs ()
204+ output = create_summary_report (
205+ probs ,
206+ reals ,
207+ renderer = "browser" ,
208+ output_file = tmp_path / "browser_report.html" ,
209+ )
210+ assert isinstance (output , Path )
211+
212+ with _serve (output .parent ) as base_url :
213+ browser = _dump_dom (f"{ base_url } /{ output .name } " )
214+
215+ _assert_report_rendered (browser )
115216
116217
117218def test_browser_summary_report_preserves_component_local_identity (tmp_path ):
0 commit comments