py/tests/test_gallery_pane.py
"""The Gallery pane must MOUNT every shipped component — not merely not crash.
`AppTest` runs the app server-side, so nothing here proves a pixel. What it does
prove is the half that a browser test cannot isolate: that the pane reaches every
component the installed wheel declares, hands each data-driven one its payload, and
hands the fixture-only ones NOTHING.
That last clause is the one worth a test. A component with no data seam that is given
a payload renders its own fixture and looks exactly like success — the same failure
the loader's refusal path exists for, and the same one that once put a knut/korp
table where the Sweden map belonged. So the pane's silence about those components is
asserted, not assumed.
The Rust side (`xtask/tests/gallery_covers_the_manifest.rs`) proves the payload TABLE
covers the registry and that every payload builds through the real adapter. This
proves the PANE actually uses it. Neither implies the other.
"""
from __future__ import annotations
import pathlib
from typing import Any
import pytest
from streamlit.testing.v1 import AppTest
import facett
import facett_demo
from facett_demo.demo_payloads import DEMO_PAYLOADS, FEATURE_PAYLOADS, FIXTURE_ONLY
#: The app UNDER TEST — inside the installed package (`cargo ops test-demo` installs the
#: wheel), so the suite drives what `pip install facett-demo` delivers.
APP = str(pathlib.Path(facett_demo.__file__).resolve().parent / "app.py")
@pytest.fixture
def mounts(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]:
"""Spy on every `facett_component` call the app makes.
Patched on the PANE's own reference, because that is the name the pane resolves
at call time — patching the host module only would leave the already-imported
binding untouched and this fixture would record nothing while passing.
"""
seen: list[dict[str, Any]] = []
def _spy(name: str, *, data: Any | None = None, height: int = 520, key: str | None = None):
seen.append({"name": name, "data": data, "height": height, "key": key})
return None
from facett_demo.panes import gallery_pane
monkeypatch.setattr(gallery_pane, "facett_component", _spy)
return seen
def _run_gallery_showing_everything(at: AppTest) -> None:
"""Put every group tab into 'the whole group' mode and re-run."""
at.run()
# The radio is the pane's only mode switch; selecting the all-at-once value makes
# one pass mount every member of every group.
for radio in at.radio:
if radio.key == "gallery-mode":
radio.set_value("the whole group")
at.run()
def test_the_gallery_mounts_every_component_the_wheel_ships(mounts):
at = AppTest.from_file(APP, default_timeout=180)
_run_gallery_showing_everything(at)
assert not at.exception, [str(e.value) for e in at.exception]
shipped = {c["name"] for c in facett.manifest()}
mounted = {m["name"] for m in mounts}
missing = shipped - mounted
assert not missing, (
f"the installed wheel ships these components and the gallery never mounted "
f"them: {sorted(missing)}"
)
def test_every_data_driven_component_got_its_payload(mounts):
at = AppTest.from_file(APP, default_timeout=180)
_run_gallery_showing_everything(at)
by_name = {m["name"]: m for m in mounts}
for name, payload in DEMO_PAYLOADS.items():
assert name in by_name, f"`{name}` was never mounted"
got = by_name[name]["data"]
assert got is not None, f"`{name}` was mounted with NO data — it would show its fixture"
# The payload the component received must BE the table's, not a reshaped copy.
assert got == payload, f"`{name}` was mounted with something other than its payload"
def test_every_non_core_bundle_is_mounted_with_its_payload(mounts):
"""docs (docview, manual) and model3d ship in the wheel under `assets/<group>/`.
The gallery once listed the CORE manifest alone, so these three were in every
wheel and never on the page. Held against the INSTALLED wheel's routing table:
every non-core bundle contributes at least one mount, each with its payload.
"""
at = AppTest.from_file(APP, default_timeout=180)
_run_gallery_showing_everything(at)
assert not at.exception, [str(e.value) for e in at.exception]
by_name = {m["name"]: m for m in mounts}
shipped = facett.manifest()
groups = {g["name"] for g in facett.bundles().get("groups", [])}
assert {"core", "docs", "model3d"} <= groups, f"the wheel routes {sorted(groups)}"
for group in sorted(groups - {"core"}):
members = [c["name"] for c in shipped if c["bundle"] == group]
assert members, f"the `{group}` bundle ships no manifest entries"
for name in members:
assert name in by_name, f"`{name}` (bundle `{group}`) was never mounted"
for name, payload in FEATURE_PAYLOADS.items():
assert by_name.get(name, {}).get("data") == payload, f"`{name}` was not mounted with its payload"
def test_pick_opens_the_gallery_on_a_named_component_of_each_bundle(mounts):
"""`?pick=` preselects a component in its tab (one-at-a-time mode), so a headless
browser that cannot click can open the gallery on a docs or model3d component."""
at = AppTest.from_file(APP, default_timeout=180)
at.query_params["pick"] = "manual,model3d"
at.run()
assert not at.exception, [str(e.value) for e in at.exception]
names = [m["name"] for m in mounts]
# manual comes first in `pick`, so the ui tab (which holds both) shows manual.
assert "manual" in names, names
assert "model3d" not in names, names
at2 = AppTest.from_file(APP, default_timeout=180)
at2.query_params["pick"] = "model3d"
mounts.clear()
at2.run()
assert "model3d" in [m["name"] for m in mounts]
def test_fixture_only_components_are_given_nothing(mounts):
# The dangerous direction. Handing data to a component with no data seam makes it
# render its own fixture while the page implies it is showing ours.
at = AppTest.from_file(APP, default_timeout=180)
_run_gallery_showing_everything(at)
by_name = {m["name"]: m for m in mounts}
for name in FIXTURE_ONLY:
assert name in by_name, f"fixture-only `{name}` was never mounted at all"
assert by_name[name]["data"] is None, (
f"`{name}` has no data seam but the gallery handed it a payload — it will "
f"render its OWN fixture and the page will imply otherwise"
)
def _ast_of(module) -> "ast.Module":
import ast
import inspect
return ast.parse(inspect.getsource(module))
def test_the_gallery_needs_no_server():
# The whole reason the Gallery is the first tab. If this ever starts depending on
# korp, the demo stops being showable on a box with nothing else running.
import ast
import facett_demo.panes.gallery_pane as g
imported: set[str] = set()
for node in ast.walk(_ast_of(g)):
if isinstance(node, ast.Import):
imported.update(a.name.split(".")[0] for a in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
imported.add(node.module.split(".")[0])
assert "grpc" not in imported, "the gallery pane imports grpc"
assert not any(
m.startswith("facett_demo.client") for m in imported
), "the gallery pane reaches for the korp client"
def test_the_gallery_draws_nothing_itself():
"""The owner's rule, asserted on the pane most tempted to break it.
A showcase is exactly where someone reaches for a quick `st.line_chart`.
This walks the AST rather than grepping the source, and the difference is not
pedantry: the first version of this test grepped for the forbidden names and
FAILED on this module's own docstring, which lists them in order to forbid them.
A guard that cannot tell a call from a sentence about a call will be silenced by
whoever hits it next, and then it guards nothing.
"""
import ast
import facett_demo.panes.gallery_pane as g
# Renderer libraries: an import is enough. There is no innocent reason to have
# one in a module whose pixels come from wasm.
banned_modules = {"pandas", "plotly", "altair", "numpy", "matplotlib", "pydeck", "networkx"}
# Streamlit's own chart/table primitives: each one is a facett component
# reimplemented in the host.
banned_calls = {
"line_chart", "bar_chart", "area_chart", "scatter_chart", "map",
"pyplot", "altair_chart", "plotly_chart", "graphviz_chart", "vega_lite_chart",
"dataframe", "table", "metric", "image",
}
tree = _ast_of(g)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for a in node.names:
assert a.name.split(".")[0] not in banned_modules, (
f"the gallery pane imports `{a.name}` — that is a renderer, and every "
f"pixel on this page must come from the wasm"
)
elif isinstance(node, ast.ImportFrom) and node.module:
assert node.module.split(".")[0] not in banned_modules, (
f"the gallery pane imports from `{node.module}`"
)
elif isinstance(node, ast.Call):
f = node.func
if (
isinstance(f, ast.Attribute)
and isinstance(f.value, ast.Name)
and f.value.id == "st"
and f.attr in banned_calls
):
assert False, (
f"the gallery pane calls `st.{f.attr}()` — facett already ships that "
f"surface as a wasm component; using streamlit's is reimplementing it "
f"in the host, which is the one thing this package exists to not do"
)
def test_that_guard_would_actually_fire():
"""The guard above, driven RED — on a module that really does draw.
Without this, `test_the_gallery_draws_nothing_itself` is a test that has only ever
been run on a file that passes. It already gave a wrong answer once (it failed on
a docstring), so its second version does not get to be trusted on reputation.
"""
import ast
drawing = ast.parse(
"import streamlit as st\n"
"import pandas as pd\n"
"def render():\n"
" st.line_chart({'a': [1, 2, 3]})\n"
)
banned_modules = {"pandas", "plotly", "altair", "numpy"}
banned_calls = {"line_chart", "bar_chart", "dataframe"}
hits = []
for node in ast.walk(drawing):
if isinstance(node, ast.Import):
hits += [a.name for a in node.names if a.name.split(".")[0] in banned_modules]
elif isinstance(node, ast.Call):
f = node.func
if (
isinstance(f, ast.Attribute)
and isinstance(f.value, ast.Name)
and f.value.id == "st"
and f.attr in banned_calls
):
hits.append(f"st.{f.attr}")
assert "pandas" in hits, "the import scan is blind"
assert "st.line_chart" in hits, "the call scan is blind"
def test_no_pane_imports_a_renderer():
"""NO pane may import pandas / plotly / altair / numpy / matplotlib / pydeck.
## A correction to the audit this lane was briefed from
The 2026-09-05 t14s audit listed `panes/population_pane.py` as *"165 lines
pandas+plotly … draws charts in JS via Plotly = same banned class"*, and row 8 was
scheduled to replace it. **That is stale.** Measured today, on this tree:
* `population_pane.py` imports exactly `streamlit` and `..facet_host` — no
pandas, no plotly, nothing else;
* its own module docstring says so: *"It previously used pandas + plotly +
st.dataframe"* — past tense, and the conversion is done;
* it mounts `table` and `stat-grid` through `facett_component`, like every
other pane;
* across all of `py/`, there is not one call to `st.line_chart`,
`st.bar_chart`, `st.plotly_chart`, `st.dataframe`, `st.table` or `st.metric`.
Somebody did that work before this lane started, and the audit was written
against an older tree. Reporting it the other way round — "found and fixed a
Plotly renderer" — would have been claiming credit for a fix that already
existed.
So this test expects the empty set, not one known offender. An earlier draft of
it expected `["population_pane.py"]` and FAILED, which is how the staleness was
found: the guard disagreed with the briefing and the guard was right.
"""
import ast
import pathlib
panes_dir = pathlib.Path(facett_demo.__file__).resolve().parent
banned_modules = {"pandas", "plotly", "altair", "numpy", "matplotlib", "pydeck", "networkx"}
offenders: dict[str, list[str]] = {}
for p in sorted(panes_dir.rglob("*.py")):
tree = ast.parse(p.read_text(encoding="utf-8"))
for node in ast.walk(tree):
names: list[str] = []
if isinstance(node, ast.Import):
names = [a.name.split(".")[0] for a in node.names]
elif isinstance(node, ast.ImportFrom) and node.module:
names = [node.module.split(".")[0]]
bad = [n for n in names if n in banned_modules]
if bad:
offenders.setdefault(str(p.relative_to(panes_dir)), []).extend(bad)
assert offenders == {}, (
f"these modules import a renderer: {offenders}. facett already ships every one "
f"of those surfaces as a wasm component — mount it instead."
)
def test_no_module_in_the_app_calls_a_streamlit_chart_or_table():
"""The other half: streamlit's own primitives are facett components by proxy.
`st.dataframe` is `facett-table` (1 483 LOC) reimplemented in the host;
`st.line_chart` is `facett-plot`; `st.map` is `facett-geomap`. Importing nothing
and still calling these would satisfy the import scan above.
"""
import ast
import pathlib
app_dir = pathlib.Path(facett_demo.__file__).resolve().parent
banned = {
"line_chart", "bar_chart", "area_chart", "scatter_chart", "map",
"pyplot", "altair_chart", "plotly_chart", "graphviz_chart", "vega_lite_chart",
"dataframe", "table", "metric",
}
hits: list[str] = []
for p in sorted(app_dir.rglob("*.py")):
tree = ast.parse(p.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if isinstance(node, ast.Call):
f = node.func
if (
isinstance(f, ast.Attribute)
and isinstance(f.value, ast.Name)
and f.value.id == "st"
and f.attr in banned
):
hits.append(f"{p.relative_to(app_dir)}:{node.lineno} st.{f.attr}")
assert hits == [], f"streamlit renderers in the app: {hits}"
# ── size + generator (lane F, 2026-10-02) ──────────────────────────────────────────
def _generated() -> list[dict[str, Any]]:
return [c for c in facett.manifest() if c.get("generate")]
def test_the_wheel_ships_the_demos_generated_graph_components():
"""`universe`, `decograph` and `graphpan` (the owner's three) are in the shipped
manifest WITH a generator seam, and the universe reaches 1M, the others 100k."""
by = {c["name"]: c for c in _generated()}
for name in ("universe", "decograph", "graphpan", "graphnav", "graph3d", "graphview", "lineage"):
assert name in by, f"`{name}` ships no generator seam: {sorted(by)}"
keys = [s["key"] for s in by[name]["generate"]["sources"]]
assert "sbm" in keys and "demo" not in keys, (name, keys)
assert by["universe"]["generate"]["max"] == 1_000_000
assert by["graphpan"]["generate"]["max"] == 100_000
assert 100_000 in by["decograph"]["generate"]["sizes"]
def test_gen_link_preselects_the_generator_request(mounts):
"""`?gen=sbm:100000:1&pick=graphpan` mounts graphpan with the generator REQUEST —
three numbers, not 100 000 nodes — exactly the demo's deep link."""
at = AppTest.from_file(APP, default_timeout=180)
at.query_params["pick"] = "graphpan"
at.query_params["gen"] = "sbm:100000:1"
at.run()
assert not at.exception, [str(e.value) for e in at.exception]
got = [m for m in mounts if m["name"] == "graphpan"]
assert got, [m["name"] for m in mounts]
assert got[0]["data"] == {"generate": {"source": "sbm", "size": 100_000, "seed": 1}}, got[0]["data"]
assert len(str(got[0]["data"])) < 200, "a request, never the graph itself"
def test_the_picker_offers_the_manifests_sizes_and_drives_the_request(mounts):
at = AppTest.from_file(APP, default_timeout=180)
at.query_params["pick"] = "graphpan"
at.run()
src = next(s for s in at.selectbox if s.key == "one-graph-graphpan-gen-source")
assert src.value == "gallery payload", "nothing changes until a generator is picked"
src.set_value("tree")
at.run()
size = next(s for s in at.select_slider if s.key == "one-graph-graphpan-gen-size")
assert list(size.options)[-1] in ("100k", "100000"), size.options
size.set_value(100_000)
at.run()
assert not at.exception, [str(e.value) for e in at.exception]
last = [m for m in mounts if m["name"] == "graphpan"][-1]
assert last["data"] == {"generate": {"source": "tree", "size": 100_000, "seed": 1}}, last["data"]
def test_the_app_is_grouped_like_the_demo():
"""The top tabs are facett-demo's groups (Grafer, Kartor, …), each opening on its
gallery components; the live korp panes sit inside their group."""
at = AppTest.from_file(APP, default_timeout=180)
at.run()
labels = [t.label for t in at.tabs]
for want in ("🕸 Grafer", "🗺 Kartor", "📊 Tabeller & diagram", "⚙ Bygg & jobb", "🎛 App & UI"):
assert want in labels, labels
assert "🎨 Komponenter" in labels, labels
assert labels.index("🕸 Grafer") < labels.index("🗺 Kartor")
def test_parse_gen_link_reads_the_demos_shape():
from facett_demo.panes.gallery_pane import parse_gen_link
assert parse_gen_link("sbm:100000:1") == {"source": "sbm", "size": 100_000, "seed": 1}
assert parse_gen_link("crates") == {"source": "crates"}
assert parse_gen_link("tree:big") is None
assert parse_gen_link("") is None