Skip to content

Commit 6d22f8b

Browse files
Set HTML document title from ReportSpec title in browser report
Extract title from ReportSpec in RtichokeBrowserReport.write_html(), HTML-escaping usable non-empty string titles and preserving exact title whitespace while falling back to "rtichoke report". Co-authored-by: uriahf <11351434+uriahf@users.noreply.github.com>
1 parent c81dddc commit 6d22f8b

3 files changed

Lines changed: 80 additions & 3 deletions

File tree

‎src/rtichoke/_report_browser.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import html
56
import json
67
import re
78
from importlib.resources import files
@@ -120,7 +121,14 @@ def write_html(self, path: str | Path) -> Path:
120121
viz_js = vendor.joinpath("rtichoke-viz.js").read_text(encoding="utf-8")
121122
viz_css = vendor.joinpath("rtichoke-viz.css").read_text(encoding="utf-8")
122123
render_fn = _resolve_render_report_symbol(viz_js)
123-
html = f"""<!doctype html>
124+
125+
raw_title = self.spec.get("title")
126+
if isinstance(raw_title, str) and raw_title.strip():
127+
doc_title = html.escape(raw_title)
128+
else:
129+
doc_title = "rtichoke report"
130+
131+
html_content = f"""<!doctype html>
124132
<html lang="en">
125133
<head>
126134
<meta charset="utf-8">
@@ -129,7 +137,7 @@ def write_html(self, path: str | Path) -> Path:
129137
{viz_css}
130138
{_summary_report_density_css()}
131139
</style>
132-
<title>rtichoke report</title>
140+
<title>{doc_title}</title>
133141
</head>
134142
<body>
135143
<div id="rtichoke-report"></div>
@@ -148,5 +156,5 @@ def write_html(self, path: str | Path) -> Path:
148156
</body>
149157
</html>
150158
"""
151-
output.write_text(html, encoding="utf-8")
159+
output.write_text(html_content, encoding="utf-8")
152160
return output

‎tests/test_report_browser.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,74 @@ def test_browser_report_does_not_delete_or_overwrite_preexisting_assets(tmp_path
164164
assert existing_css.read_text(encoding="utf-8") == "/* custom preexisting css */"
165165

166166

167+
import copy
168+
169+
170+
def test_browser_report_title_usable(tmp_path):
171+
report_spec = {"title": "Summary Report", "sections": []}
172+
output = RtichokeBrowserReport(report_spec).write_html(tmp_path / "report.html")
173+
html_content = output.read_text(encoding="utf-8")
174+
assert "<title>Summary Report</title>" in html_content
175+
176+
177+
def test_browser_report_title_preserves_unstripped_usable_string(tmp_path):
178+
report_spec = {"title": " Summary Report ", "sections": []}
179+
output = RtichokeBrowserReport(report_spec).write_html(tmp_path / "report.html")
180+
html_content = output.read_text(encoding="utf-8")
181+
assert "<title> Summary Report </title>" in html_content
182+
183+
184+
@pytest.mark.parametrize(
185+
"invalid_title",
186+
[
187+
None,
188+
"",
189+
" ",
190+
"\t\n ",
191+
123,
192+
["Summary Report"],
193+
{"a": "b"},
194+
],
195+
)
196+
def test_browser_report_title_fallback(tmp_path, invalid_title):
197+
spec = {} if invalid_title is None else {"title": invalid_title}
198+
output = RtichokeBrowserReport(spec).write_html(tmp_path / "report.html")
199+
html_content = output.read_text(encoding="utf-8")
200+
assert "<title>rtichoke report</title>" in html_content
201+
202+
203+
def test_browser_report_title_html_escaped(tmp_path):
204+
title_with_special = "Summary & Performance <Report> 'v1.0' \"Draft\""
205+
report_spec = {"title": title_with_special, "sections": []}
206+
output = RtichokeBrowserReport(report_spec).write_html(tmp_path / "report.html")
207+
html_content = output.read_text(encoding="utf-8")
208+
expected_title = "Summary &amp; Performance &lt;Report&gt; &#x27;v1.0&#x27; &quot;Draft&quot;"
209+
assert f"<title>{expected_title}</title>" in html_content
210+
211+
212+
def test_browser_report_does_not_mutate_spec(tmp_path):
213+
original_spec = {
214+
"title": "Summary Report",
215+
"sections": [],
216+
"value_nan": float("nan"),
217+
}
218+
spec_copy = copy.deepcopy(original_spec)
219+
220+
report = RtichokeBrowserReport(original_spec)
221+
output = report.write_html(tmp_path / "report.html")
222+
html_content = output.read_text(encoding="utf-8")
223+
224+
assert report.spec is original_spec
225+
# original_spec itself should not be mutated
226+
assert repr(original_spec["value_nan"]) == repr(spec_copy["value_nan"])
227+
assert original_spec["title"] == "Summary Report"
228+
229+
# Embedded spec in HTML is sanitized (NaN -> None)
230+
embedded = _embedded_report(html_content)
231+
assert embedded["title"] == "Summary Report"
232+
assert embedded["value_nan"] is None
233+
234+
167235
def test_resolve_render_report_symbol_export_clauses():
168236
# Unminified / shorthand export
169237
shorthand_js = "export { foo, renderReport, bar };"

‎tests/test_summary_report_browser.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def test_browser_summary_report_is_opt_in_and_uses_real_canonical_components(
184184
assert not (tmp_path / "rtichoke-viz.css").exists()
185185

186186
html = output.read_text(encoding="utf-8")
187+
assert "<title>Summary Report</title>" in html
187188
report = _embedded_report(html)
188189
assert report["schemaVersion"] == "1.1"
189190
assert report["type"] == "report"

0 commit comments

Comments
 (0)