app: render reports inline via iframe srcdoc (private-Space safe)
Browse filesPolish follow-up on C6. Reports now render inside the Space, inside
the same iframe HF uses to host the leaderboard, with no second
HTTP request leaving the browser.
Why: the Space is private, and HF's edge gates same-origin pathname
navigations on a short-lived __sign JWT that browsers don't carry
forward when a user clicks a relative link. The previous design
(submission_name -> /reports/<id>.html) worked server-side under
Bearer auth (verified) but 404'd in the browser. Confirmed via
external review and reproduced with a requests.Session cookie test.
This commit:
- Drops the submission_name -> /reports/<id>.html link wrapping
from the rendered table; the cell stays plain text. The
underlying `report_url` column (relative URL to the proxy) is
still computed for the modern-pipeline gate but no longer
surfaced as a clickable cell.
- Adds a `gr.HTML` viewer below the detail panel. Row-click
triggers `_format_detail_and_report`, which returns
(detail_markdown, report_iframe_html). The iframe wraps the
full report HTML via `srcdoc` so the report's CSS gets its own
document context (no Gradio flex-column clipping; explicit
`height: 90vh`).
- Keeps the FastAPI `/reports/{submission_id}.html` route as a
backdoor (works under Bearer for tooling and survives the
future public migration where it'll just become user-reachable).
Why srcdoc and not src=URL: srcdoc inlines the bytes so the
browser doesn't need to issue an authenticated second request to
the Space's edge. When the Space goes public, swap srcdoc for
src=/reports/<id>.html and the iframe keeps the same layout
isolation - 1-line change.
Verification (autonomous, no user involvement):
- 19/19 unit tests green. New tests cover the iframe handler:
modern row -> iframe-srcdoc with HTML-escaped body; pending /
failed / legacy rows -> empty viewer; fetch failure -> empty
viewer (no broken iframe); null select event -> placeholder;
attribute escaping safe against `"` and `&` in the report
content.
- Real report inspection: fetched a known modern report,
1.38 MB, 45 images all base64-inlined as PNG, no external
URLs, all expected sections present (INTERFACE OVERLAY, axis
labels, per-fixture blocks). The earlier visible cut-off was
Gradio's column flex, not the report - confirmed.
- Local boot probe: app.py serves /, the /reports/<sid>.html
backdoor returns 200 + text/html + 1.4 MB body, /reports/<bogus>
returns 404.
Post-push live probe runs next; will surface only if it fails.
- app.py +68 -35
- leaderboard.py +17 -8
- tests/test_leaderboard.py +21 -28
- tests/test_proxy.py +131 -8
|
@@ -10,6 +10,7 @@ rather than render).
|
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
|
|
|
|
| 13 |
import logging
|
| 14 |
import os
|
| 15 |
import re
|
|
@@ -92,24 +93,53 @@ def _fmt_timestamp(ts) -> str:
|
|
| 92 |
return str(ts)
|
| 93 |
|
| 94 |
|
| 95 |
-
def
|
| 96 |
-
"""
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
"""
|
| 108 |
if df is None or len(df) == 0 or evt is None or evt.index is None:
|
| 109 |
-
return DETAIL_PLACEHOLDER
|
| 110 |
idx = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
|
| 111 |
if idx < 0 or idx >= len(df):
|
| 112 |
-
return DETAIL_PLACEHOLDER
|
| 113 |
row = df.iloc[idx]
|
| 114 |
|
| 115 |
title = row.get("submission_name") or "(unnamed submission)"
|
|
@@ -122,20 +152,24 @@ def _format_detail(df: pd.DataFrame | None, evt: gr.SelectData) -> str:
|
|
| 122 |
lines.append(f"- **Submitted**: {_fmt_timestamp(row['submitted_at'])}")
|
| 123 |
if _has(row.get("notes")):
|
| 124 |
lines.append(f"- **Notes**: {row['notes']}")
|
| 125 |
-
# `model details (optional)` carries the markdown link (or
|
| 126 |
-
# _None_ when missing); the hidden `submission_blob_url` /
|
| 127 |
-
# `report_url` columns are raw URLs we wrap into named links here.
|
| 128 |
lines.append(
|
| 129 |
f"- **Model details (optional)**: "
|
| 130 |
f"{row.get('model details (optional)') or '_None_'}"
|
| 131 |
)
|
| 132 |
if _has(row.get("submission_blob_url")):
|
| 133 |
-
lines.append(
|
| 134 |
-
|
| 135 |
-
|
| 136 |
if row.get("status") == "failed" and _has(row.get("failure_reason")):
|
| 137 |
lines.append(f"- **Failure reason**: {row['failure_reason']}")
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
|
| 141 |
@lru_cache(maxsize=128)
|
|
@@ -212,25 +246,24 @@ with gr.Blocks(title="CADGenBench Leaderboard", theme=gr.themes.Soft()) as block
|
|
| 212 |
outputs=[validated_view, unvalidated_view],
|
| 213 |
)
|
| 214 |
|
| 215 |
-
# Row-click
|
| 216 |
-
#
|
| 217 |
-
#
|
| 218 |
-
#
|
| 219 |
-
#
|
|
|
|
|
|
|
| 220 |
detail_panel = gr.Markdown(
|
| 221 |
value=DETAIL_PLACEHOLDER,
|
| 222 |
label="Selected submission",
|
| 223 |
)
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
inputs=unvalidated_view,
|
| 232 |
-
outputs=detail_panel,
|
| 233 |
-
)
|
| 234 |
|
| 235 |
with gr.Tab("Submit"):
|
| 236 |
gr.Markdown(
|
|
|
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
+
import html
|
| 14 |
import logging
|
| 15 |
import os
|
| 16 |
import re
|
|
|
|
| 93 |
return str(ts)
|
| 94 |
|
| 95 |
|
| 96 |
+
def _build_report_iframe(html_bytes: bytes) -> str:
|
| 97 |
+
"""Wrap a fetched report's HTML bytes into a self-contained iframe.
|
| 98 |
|
| 99 |
+
``srcdoc`` puts the entire report HTML directly inside the iframe
|
| 100 |
+
attribute. The iframe gets its own document context, so the
|
| 101 |
+
report's CSS can't collide with Gradio's, and an explicit
|
| 102 |
+
``height: 90vh`` keeps the report from being clipped by Gradio's
|
| 103 |
+
column-flex layout (which was the visible cut-off the user saw
|
| 104 |
+
in earlier rendering attempts).
|
| 105 |
+
|
| 106 |
+
The Space is private; the FastAPI ``/reports/<id>.html`` route
|
| 107 |
+
works server-side under Bearer auth but breaks for a logged-in
|
| 108 |
+
browser user (HF's edge gates same-origin pathname navigations
|
| 109 |
+
on a JWT that the browser doesn't carry forward). srcdoc
|
| 110 |
+
sidesteps that entirely by inlining the bytes; no second HTTP
|
| 111 |
+
request leaves the browser.
|
| 112 |
+
"""
|
| 113 |
+
escaped = html.escape(
|
| 114 |
+
html_bytes.decode("utf-8", errors="replace"), quote=True,
|
| 115 |
+
)
|
| 116 |
+
return (
|
| 117 |
+
f'<iframe srcdoc="{escaped}" '
|
| 118 |
+
'style="width:100%; height:90vh; border:0; display:block;" '
|
| 119 |
+
'title="Submission report"></iframe>'
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _format_detail_and_report(
|
| 124 |
+
df: pd.DataFrame | None, evt: gr.SelectData,
|
| 125 |
+
) -> tuple[str, str]:
|
| 126 |
+
"""Return ``(detail_markdown, report_iframe_html)`` for the clicked row.
|
| 127 |
+
|
| 128 |
+
The detail panel holds metadata (submitter, status, timestamp,
|
| 129 |
+
notes, links to ZIP and external agent URL). The HTML viewer
|
| 130 |
+
holds the rendered report when one exists (status == completed
|
| 131 |
+
AND the row carries the modern-pipeline ``submission_sha256``
|
| 132 |
+
sentinel) - the leaderboard reader computes ``report_url`` only
|
| 133 |
+
for rows that satisfy that gate.
|
| 134 |
+
|
| 135 |
+
Returns ``(DETAIL_PLACEHOLDER, "")`` on a null / out-of-range
|
| 136 |
+
event so the panel falls back to its initial state.
|
| 137 |
"""
|
| 138 |
if df is None or len(df) == 0 or evt is None or evt.index is None:
|
| 139 |
+
return DETAIL_PLACEHOLDER, ""
|
| 140 |
idx = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
|
| 141 |
if idx < 0 or idx >= len(df):
|
| 142 |
+
return DETAIL_PLACEHOLDER, ""
|
| 143 |
row = df.iloc[idx]
|
| 144 |
|
| 145 |
title = row.get("submission_name") or "(unnamed submission)"
|
|
|
|
| 152 |
lines.append(f"- **Submitted**: {_fmt_timestamp(row['submitted_at'])}")
|
| 153 |
if _has(row.get("notes")):
|
| 154 |
lines.append(f"- **Notes**: {row['notes']}")
|
|
|
|
|
|
|
|
|
|
| 155 |
lines.append(
|
| 156 |
f"- **Model details (optional)**: "
|
| 157 |
f"{row.get('model details (optional)') or '_None_'}"
|
| 158 |
)
|
| 159 |
if _has(row.get("submission_blob_url")):
|
| 160 |
+
lines.append(
|
| 161 |
+
f"- **Submission ZIP**: [download]({row['submission_blob_url']})"
|
| 162 |
+
)
|
| 163 |
if row.get("status") == "failed" and _has(row.get("failure_reason")):
|
| 164 |
lines.append(f"- **Failure reason**: {row['failure_reason']}")
|
| 165 |
+
detail_md = "\n".join(lines)
|
| 166 |
+
|
| 167 |
+
report_iframe = ""
|
| 168 |
+
if _has(row.get("report_url")) and _has(row.get("submission_id")):
|
| 169 |
+
content = _fetch_report_html(str(row["submission_id"]))
|
| 170 |
+
if content:
|
| 171 |
+
report_iframe = _build_report_iframe(content)
|
| 172 |
+
return detail_md, report_iframe
|
| 173 |
|
| 174 |
|
| 175 |
@lru_cache(maxsize=128)
|
|
|
|
| 246 |
outputs=[validated_view, unvalidated_view],
|
| 247 |
)
|
| 248 |
|
| 249 |
+
# Row-click panel: one shared metadata markdown component +
|
| 250 |
+
# one report viewer below it. The viewer holds an iframe
|
| 251 |
+
# containing the full per-submission report (srcdoc-inlined
|
| 252 |
+
# so no second HTTP request needs to leave the browser - the
|
| 253 |
+
# Space is private and HF's edge would 404 same-origin
|
| 254 |
+
# pathname navigations that aren't carrying the iframe's
|
| 255 |
+
# short-lived `__sign` JWT). Both tables share both outputs.
|
| 256 |
detail_panel = gr.Markdown(
|
| 257 |
value=DETAIL_PLACEHOLDER,
|
| 258 |
label="Selected submission",
|
| 259 |
)
|
| 260 |
+
report_viewer = gr.HTML(value="", label="Report")
|
| 261 |
+
for view in (validated_view, unvalidated_view):
|
| 262 |
+
view.select(
|
| 263 |
+
fn=_format_detail_and_report,
|
| 264 |
+
inputs=view,
|
| 265 |
+
outputs=[detail_panel, report_viewer],
|
| 266 |
+
)
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
with gr.Tab("Submit"):
|
| 269 |
gr.Markdown(
|
|
@@ -247,17 +247,26 @@ def _report_relative_url(submission_id, status, submission_sha256) -> str:
|
|
| 247 |
|
| 248 |
|
| 249 |
def _submission_name_md(name, report_url) -> str:
|
| 250 |
-
"""Render `submission_name` as
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
"""
|
| 256 |
if _is_empty(name):
|
| 257 |
return "(unnamed submission)"
|
| 258 |
-
|
| 259 |
-
return str(name)
|
| 260 |
-
return f"[{name}]({report_url})"
|
| 261 |
|
| 262 |
|
| 263 |
def load_leaderboard_split() -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
|
|
| 247 |
|
| 248 |
|
| 249 |
def _submission_name_md(name, report_url) -> str:
|
| 250 |
+
"""Render `submission_name` as plain text.
|
| 251 |
+
|
| 252 |
+
Earlier iterations wrapped this as a markdown link to the report
|
| 253 |
+
proxy at ``/reports/<id>.html``. On a private Space the link
|
| 254 |
+
works server-side (Bearer auth) but breaks for a logged-in user
|
| 255 |
+
in the browser: the iframe's `__sign` token doesn't propagate to
|
| 256 |
+
same-origin pathname navigations, and HF's edge returns 404
|
| 257 |
+
before the request reaches the FastAPI route. Reports are now
|
| 258 |
+
rendered inline via an iframe-srcdoc viewer below the detail
|
| 259 |
+
panel (see ``_format_detail_and_report`` in :mod:`app`), so the
|
| 260 |
+
submission_name cell stays plain text to avoid offering a link
|
| 261 |
+
that doesn't work.
|
| 262 |
+
|
| 263 |
+
`report_url` is accepted (and ignored) so the call sites don't
|
| 264 |
+
have to change when the Space eventually goes public and the
|
| 265 |
+
link can be restored.
|
| 266 |
"""
|
| 267 |
if _is_empty(name):
|
| 268 |
return "(unnamed submission)"
|
| 269 |
+
return str(name)
|
|
|
|
|
|
|
| 270 |
|
| 271 |
|
| 272 |
def load_leaderboard_split() -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
@@ -133,39 +133,32 @@ def test_empty_input_returns_two_empty_frames(monkeypatch):
|
|
| 133 |
assert list(unvalidated.columns) == leaderboard.LEADERBOARD_COLS
|
| 134 |
|
| 135 |
|
| 136 |
-
def
|
| 137 |
-
"""`submission_name`
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
| 142 |
"""
|
| 143 |
monkeypatch.setattr(leaderboard, "_load_rows_from_hub", lambda: _stub_rows())
|
| 144 |
validated, unvalidated = leaderboard.load_leaderboard_split()
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
alpha = validated.iloc[0]
|
| 148 |
-
assert alpha["
|
| 149 |
-
# Beta likewise (also completed, has sha256).
|
| 150 |
-
beta = unvalidated[
|
| 151 |
-
unvalidated["submission_name"].str.contains("Beta Agent v2", regex=False)
|
| 152 |
-
].iloc[0]
|
| 153 |
-
assert beta["submission_name"] == "[Beta Agent v2](/reports/sub-b.html)"
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
def test_legacy_row_submission_name_is_plain_text(monkeypatch):
|
| 157 |
-
"""Rows without submission_sha256 (legacy seeds) keep plain-text names.
|
| 158 |
-
|
| 159 |
-
No `reports/<id>.html` exists on the dataset for those rows, so
|
| 160 |
-
we don't wrap the name in a link.
|
| 161 |
-
"""
|
| 162 |
-
monkeypatch.setattr(leaderboard, "_load_rows_from_hub", lambda: _stub_rows())
|
| 163 |
-
_, unvalidated = leaderboard.load_leaderboard_split()
|
| 164 |
gamma = unvalidated[unvalidated["submission_name"] == "Gamma baseline"].iloc[0]
|
| 165 |
-
# Plain text: no `[...](...)` wrapping.
|
| 166 |
-
assert "[" not in gamma["submission_name"]
|
| 167 |
-
assert "](" not in gamma["submission_name"]
|
| 168 |
-
# And the hidden report_url column is empty for the same reason.
|
| 169 |
assert gamma["report_url"] == ""
|
| 170 |
|
| 171 |
|
|
|
|
| 133 |
assert list(unvalidated.columns) == leaderboard.LEADERBOARD_COLS
|
| 134 |
|
| 135 |
|
| 136 |
+
def test_submission_name_is_plain_text(monkeypatch):
|
| 137 |
+
"""`submission_name` cells render as plain text on both tables.
|
| 138 |
+
|
| 139 |
+
Earlier iterations wrapped the name as a markdown link to the
|
| 140 |
+
report proxy, but the proxy URL 404s for logged-in users on a
|
| 141 |
+
private Space (no auth carryover on same-origin pathname
|
| 142 |
+
navigations). Reports are rendered inline via an iframe viewer
|
| 143 |
+
on row-click instead, so the name cell stays plain text.
|
| 144 |
"""
|
| 145 |
monkeypatch.setattr(leaderboard, "_load_rows_from_hub", lambda: _stub_rows())
|
| 146 |
validated, unvalidated = leaderboard.load_leaderboard_split()
|
| 147 |
+
for name in [
|
| 148 |
+
"Alpha Agent v1",
|
| 149 |
+
"Beta Agent v2",
|
| 150 |
+
"Gamma baseline",
|
| 151 |
+
]:
|
| 152 |
+
present = (
|
| 153 |
+
(validated["submission_name"] == name).any()
|
| 154 |
+
or (unvalidated["submission_name"] == name).any()
|
| 155 |
+
)
|
| 156 |
+
assert present, f"{name!r} should be present as a plain-text cell"
|
| 157 |
+
# The hidden `report_url` column is still computed (used by the
|
| 158 |
+
# row-click handler to decide whether to embed the report iframe).
|
| 159 |
alpha = validated.iloc[0]
|
| 160 |
+
assert alpha["report_url"] == "/reports/sub-a.html"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
gamma = unvalidated[unvalidated["submission_name"] == "Gamma baseline"].iloc[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
assert gamma["report_url"] == ""
|
| 163 |
|
| 164 |
|
|
@@ -1,14 +1,28 @@
|
|
| 1 |
-
"""Unit tests for the
|
| 2 |
-
|
| 3 |
-
The Space exposes
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
from __future__ import annotations
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
import app
|
| 13 |
|
| 14 |
|
|
@@ -58,3 +72,112 @@ def test_proxy_route_is_registered():
|
|
| 58 |
"""
|
| 59 |
routes = [getattr(r, "path", None) for r in app.app.routes]
|
| 60 |
assert "/reports/{submission_id}.html" in routes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for the report-proxy route and the inline iframe viewer.
|
| 2 |
+
|
| 3 |
+
The Space exposes two paths for the per-submission HTML report:
|
| 4 |
+
|
| 5 |
+
- ``/reports/{submission_id}.html`` (FastAPI route): re-serves the
|
| 6 |
+
file with ``Content-Type: text/html``. Works under Bearer auth
|
| 7 |
+
for programmatic clients; gets 404'd by HF's edge for logged-in
|
| 8 |
+
browser users on a private Space (no auth carryover across
|
| 9 |
+
same-origin pathname navigations). Kept as a backdoor / for the
|
| 10 |
+
future public migration.
|
| 11 |
+
- ``_format_detail_and_report`` (row-click handler): server-side
|
| 12 |
+
fetches the report via ``hf_hub_download`` and inlines it into
|
| 13 |
+
an ``<iframe srcdoc=...>``. No browser HTTP request → no edge
|
| 14 |
+
auth gate → renders for any logged-in user.
|
| 15 |
+
|
| 16 |
+
Tests stub the Hub fetch via monkeypatch so the suite has zero
|
| 17 |
+
network I/O.
|
| 18 |
"""
|
| 19 |
from __future__ import annotations
|
| 20 |
|
| 21 |
+
import re
|
| 22 |
+
import types
|
| 23 |
+
|
| 24 |
+
import pandas as pd
|
| 25 |
+
|
| 26 |
import app
|
| 27 |
|
| 28 |
|
|
|
|
| 72 |
"""
|
| 73 |
routes = [getattr(r, "path", None) for r in app.app.routes]
|
| 74 |
assert "/reports/{submission_id}.html" in routes
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# --- Inline iframe viewer (_format_detail_and_report) ----------------
|
| 78 |
+
|
| 79 |
+
def _stub_row(**overrides):
|
| 80 |
+
base = {
|
| 81 |
+
"submission_id": "sub-test-x",
|
| 82 |
+
"submission_name": "Test Agent",
|
| 83 |
+
"submitter_name": "team-test",
|
| 84 |
+
"status": "completed",
|
| 85 |
+
"submitted_at": "2026-05-26T12:02:31Z",
|
| 86 |
+
"notes": None,
|
| 87 |
+
"model details (optional)": "_None_",
|
| 88 |
+
"submission_blob_url": "https://example.test/sub-test-x.zip",
|
| 89 |
+
"report_url": "/reports/sub-test-x.html",
|
| 90 |
+
"failure_reason": None,
|
| 91 |
+
}
|
| 92 |
+
base.update(overrides)
|
| 93 |
+
return pd.DataFrame([base])
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _fake_evt(idx=0):
|
| 97 |
+
"""Minimal stand-in for gr.SelectData with the .index attr we read."""
|
| 98 |
+
return types.SimpleNamespace(index=[idx, 0])
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def test_iframe_viewer_inlines_report_for_modern_row(monkeypatch):
|
| 102 |
+
"""A completed modern row's HTML lands inside <iframe srcdoc>.
|
| 103 |
+
|
| 104 |
+
Confirms the fetched bytes are HTML-escaped (so `<html>` is not
|
| 105 |
+
re-parsed by the host page) and the iframe carries explicit
|
| 106 |
+
sizing so Gradio's column flex can't clip it vertically.
|
| 107 |
+
"""
|
| 108 |
+
monkeypatch.setattr(
|
| 109 |
+
app, "_fetch_report_html",
|
| 110 |
+
lambda sid: b"<!DOCTYPE html><body><h1>Report for " + sid.encode() + b"</h1></body>",
|
| 111 |
+
)
|
| 112 |
+
df = _stub_row()
|
| 113 |
+
md, iframe = app._format_detail_and_report(df, _fake_evt())
|
| 114 |
+
assert "### Test Agent" in md
|
| 115 |
+
assert iframe.startswith('<iframe srcdoc="')
|
| 116 |
+
# HTML-escaped content: literal "<!DOCTYPE html>" becomes
|
| 117 |
+
# "<!DOCTYPE html>" inside the attribute.
|
| 118 |
+
assert "<!DOCTYPE html>" in iframe
|
| 119 |
+
assert "sub-test-x" in iframe
|
| 120 |
+
assert 'style="width:100%; height:90vh; border:0; display:block;"' in iframe
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_iframe_viewer_empty_for_pending_or_failed_row(monkeypatch):
|
| 124 |
+
"""Rows without a report_url get an empty viewer (no iframe at all).
|
| 125 |
+
|
| 126 |
+
Pending: still evaluating; no report exists yet. Failed: eval
|
| 127 |
+
crashed; no report uploaded. Legacy: pre-modern-pipeline; the
|
| 128 |
+
file genuinely doesn't exist on the dataset. All three are
|
| 129 |
+
handled by the same `report_url == ""` gate.
|
| 130 |
+
"""
|
| 131 |
+
monkeypatch.setattr(app, "_fetch_report_html", lambda sid: b"unused")
|
| 132 |
+
for row_overrides in [
|
| 133 |
+
{"status": "pending", "report_url": ""},
|
| 134 |
+
{"status": "failed", "report_url": "", "failure_reason": "boom"},
|
| 135 |
+
{"status": "completed", "report_url": ""}, # legacy
|
| 136 |
+
]:
|
| 137 |
+
df = _stub_row(**row_overrides)
|
| 138 |
+
md, iframe = app._format_detail_and_report(df, _fake_evt())
|
| 139 |
+
assert iframe == "", f"expected empty viewer for {row_overrides}"
|
| 140 |
+
# The metadata panel still renders.
|
| 141 |
+
assert "### Test Agent" in md
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_iframe_viewer_falls_back_to_empty_when_fetch_fails(monkeypatch):
|
| 145 |
+
"""If _fetch_report_html returns None (Hub blip), no iframe is emitted.
|
| 146 |
+
|
| 147 |
+
Avoids surfacing a broken iframe on a transient failure.
|
| 148 |
+
"""
|
| 149 |
+
monkeypatch.setattr(app, "_fetch_report_html", lambda sid: None)
|
| 150 |
+
df = _stub_row()
|
| 151 |
+
_md, iframe = app._format_detail_and_report(df, _fake_evt())
|
| 152 |
+
assert iframe == ""
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def test_iframe_viewer_returns_placeholder_on_null_event():
|
| 156 |
+
"""A null SelectData (no row clicked) returns placeholder + empty viewer."""
|
| 157 |
+
df = _stub_row()
|
| 158 |
+
fake = types.SimpleNamespace(index=None)
|
| 159 |
+
md, iframe = app._format_detail_and_report(df, fake)
|
| 160 |
+
assert md == app.DETAIL_PLACEHOLDER
|
| 161 |
+
assert iframe == ""
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def test_iframe_escape_is_attribute_safe(monkeypatch):
|
| 165 |
+
"""Quotes / ampersands inside the report HTML are escaped properly.
|
| 166 |
+
|
| 167 |
+
A `"` inside the report would otherwise terminate the srcdoc
|
| 168 |
+
attribute prematurely and break parsing. Regression guard.
|
| 169 |
+
"""
|
| 170 |
+
monkeypatch.setattr(
|
| 171 |
+
app, "_fetch_report_html",
|
| 172 |
+
lambda sid: b'<html><body>tag: <a href="https://x.test">x</a> & co.</body></html>',
|
| 173 |
+
)
|
| 174 |
+
df = _stub_row()
|
| 175 |
+
_md, iframe = app._format_detail_and_report(df, _fake_evt())
|
| 176 |
+
# Within the srcdoc value, double-quotes must be HTML-escaped.
|
| 177 |
+
srcdoc = re.search(r'srcdoc="(.*)"\s+style=', iframe, re.DOTALL)
|
| 178 |
+
assert srcdoc is not None
|
| 179 |
+
inner = srcdoc.group(1)
|
| 180 |
+
# No unescaped " inside the attribute value.
|
| 181 |
+
assert '"' not in inner
|
| 182 |
+
assert """ in inner
|
| 183 |
+
assert "&" in inner
|