Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/rtichoke/_report_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import html
import json
import re
from importlib.resources import files
Expand Down Expand Up @@ -120,7 +121,14 @@ def write_html(self, path: str | Path) -> Path:
viz_js = vendor.joinpath("rtichoke-viz.js").read_text(encoding="utf-8")
viz_css = vendor.joinpath("rtichoke-viz.css").read_text(encoding="utf-8")
render_fn = _resolve_render_report_symbol(viz_js)
html = f"""<!doctype html>

raw_title = self.spec.get("title")
if isinstance(raw_title, str) and raw_title.strip():
doc_title = html.escape(raw_title)
else:
doc_title = "rtichoke report"

html_content = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
Expand All @@ -129,7 +137,7 @@ def write_html(self, path: str | Path) -> Path:
{viz_css}
{_summary_report_density_css()}
</style>
<title>rtichoke report</title>
<title>{doc_title}</title>
</head>
<body>
<div id="rtichoke-report"></div>
Expand All @@ -148,5 +156,5 @@ def write_html(self, path: str | Path) -> Path:
</body>
</html>
"""
output.write_text(html, encoding="utf-8")
output.write_text(html_content, encoding="utf-8")
return output
68 changes: 68 additions & 0 deletions tests/test_report_browser.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
import json
from importlib.resources import files
from typing import Any, cast
Expand Down Expand Up @@ -164,6 +165,73 @@ def test_browser_report_does_not_delete_or_overwrite_preexisting_assets(tmp_path
assert existing_css.read_text(encoding="utf-8") == "/* custom preexisting css */"


def test_browser_report_title_usable(tmp_path):
report_spec = {"title": "Summary Report", "sections": []}
output = RtichokeBrowserReport(report_spec).write_html(tmp_path / "report.html")
html_content = output.read_text(encoding="utf-8")
assert "<title>Summary Report</title>" in html_content


def test_browser_report_title_preserves_unstripped_usable_string(tmp_path):
report_spec = {"title": " Summary Report ", "sections": []}
output = RtichokeBrowserReport(report_spec).write_html(tmp_path / "report.html")
html_content = output.read_text(encoding="utf-8")
assert "<title> Summary Report </title>" in html_content


@pytest.mark.parametrize(
"invalid_title",
[
None,
"",
" ",
"\t\n ",
123,
["Summary Report"],
{"a": "b"},
],
)
def test_browser_report_title_fallback(tmp_path, invalid_title):
spec = {} if invalid_title is None else {"title": invalid_title}
output = RtichokeBrowserReport(spec).write_html(tmp_path / "report.html")
html_content = output.read_text(encoding="utf-8")
assert "<title>rtichoke report</title>" in html_content


def test_browser_report_title_html_escaped(tmp_path):
title_with_special = "Summary & Performance <Report> 'v1.0' \"Draft\""
report_spec = {"title": title_with_special, "sections": []}
output = RtichokeBrowserReport(report_spec).write_html(tmp_path / "report.html")
html_content = output.read_text(encoding="utf-8")
expected_title = (
"Summary &amp; Performance &lt;Report&gt; &#x27;v1.0&#x27; &quot;Draft&quot;"
)
assert f"<title>{expected_title}</title>" in html_content


def test_browser_report_does_not_mutate_spec(tmp_path):
original_spec = {
"title": "Summary Report",
"sections": [],
"value_nan": float("nan"),
}
spec_copy = copy.deepcopy(original_spec)

report = RtichokeBrowserReport(original_spec)
output = report.write_html(tmp_path / "report.html")
html_content = output.read_text(encoding="utf-8")

assert report.spec is original_spec
# original_spec itself should not be mutated
assert repr(original_spec["value_nan"]) == repr(spec_copy["value_nan"])
assert original_spec["title"] == "Summary Report"

# Embedded spec in HTML is sanitized (NaN -> None)
embedded = _embedded_report(html_content)
assert embedded["title"] == "Summary Report"
assert embedded["value_nan"] is None


def test_resolve_render_report_symbol_export_clauses():
# Unminified / shorthand export
shorthand_js = "export { foo, renderReport, bar };"
Expand Down
1 change: 1 addition & 0 deletions tests/test_summary_report_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ def test_browser_summary_report_is_opt_in_and_uses_real_canonical_components(
assert not (tmp_path / "rtichoke-viz.css").exists()

html = output.read_text(encoding="utf-8")
assert "<title>Summary Report</title>" in html
report = _embedded_report(html)
assert report["schemaVersion"] == "1.1"
assert report["type"] == "report"
Expand Down
Loading