diff --git a/tests/test_facet_display.py b/tests/test_facet_display.py new file mode 100644 index 0000000..9f36223 --- /dev/null +++ b/tests/test_facet_display.py @@ -0,0 +1,76 @@ +"""A multi-valued facet must render as separate values, not one blob. + +`build.py` filters facets with `str(value).split()`, so a facet is +multi-valued *by contract*. The package page rendered the raw value instead, so +a two-value facet came out as `tui web-ui` -- read as one odd token rather than +two. Filtering and display have to split the same way. +""" + +from __future__ import annotations + +import os +import re + +import pytest + +from xpkgindex.build import build +from xpkgindex.render import render + +from conftest import commit, init_repo, write_config, write_descriptor, write_plugin + +PLUGIN = ''' + from xpkgindex.models import Facet + from xpkgindex.plugins import Plugin + + class P(Plugin): + api_version = 1 + name = "multi" + + def on_package(self, pkg, raw): + pkg.facets["kind"] = "tui web-ui" + pkg.facets["single"] = "solo" + + def facets(self): + return [Facet(key="kind", label="kind", weight=10), + Facet(key="single", label="single", weight=20)] +''' + + +@pytest.fixture +def site(tmp_path): + root = str(tmp_path / "index") + os.makedirs(root) + init_repo(root) + rel = write_plugin(root, PLUGIN) + write_config(root, plugins=[rel]) + write_descriptor(root, "alpha", "widget") + commit(root, "add widget", date="2026-01-01") + out = str(tmp_path / "site") + data, config = build(root, offline=True) + render(data, config, out) + with open(os.path.join(out, "packages", "widget", "index.html"), + encoding="utf-8") as f: + return f.read() + + +def _facet_block(html: str, key: str) -> str: + m = re.search(rf"
{key}
\s*
(.*?)
", html, re.S) + assert m, f"facet {key!r} is not on the page" + return m.group(1) + + +def test_multi_valued_facet_renders_each_value_separately(site): + block = _facet_block(site, "kind") + values = re.findall(r"([^<]+)", block) + assert values == ["tui", "web-ui"], \ + f"expected two rendered values, got {values!r}" + + +def test_a_single_valued_facet_is_unchanged(site): + block = _facet_block(site, "single") + assert re.findall(r"([^<]+)", block) == ["solo"] + + +def test_the_joined_form_never_reaches_the_page(site): + """The bug's signature: both values inside one element.""" + assert "tui web-ui" not in re.sub(r"\s+", " ", site) diff --git a/xpkgindex/render.py b/xpkgindex/render.py index a763d55..4dfbfa6 100644 --- a/xpkgindex/render.py +++ b/xpkgindex/render.py @@ -52,6 +52,10 @@ def _env(lang: str = i18n.DEFAULT, default: str = i18n.DEFAULT) -> Environment: lstrip_blocks=True, ) env.filters["platform_label"] = lambda p: PLATFORM_LABELS.get(p, p) + # Facets are multi-valued by contract: build.py filters them with + # `str(value).split()`. Templates must split the same way, or a two-value + # facet renders as one blob -- "tui web-ui" reading as a single odd token. + env.filters["split_facet"] = lambda v: str(v).split() # `loc` unwraps any consumer- or plugin-supplied value that was written # per locale. Applied wherever such text reaches a template. env.filters["loc"] = lambda v: i18n.localize(v, lang, default) diff --git a/xpkgindex/templates/package.html b/xpkgindex/templates/package.html index b6e1426..9eecea5 100644 --- a/xpkgindex/templates/package.html +++ b/xpkgindex/templates/package.html @@ -103,7 +103,11 @@

{{ t('pkg.facts') }}

{% for key, value in pkg.facets.items() %}
{{ key }}
- {{ value }} + {# A facet is multi-valued by contract -- build.py filters with + `str(value).split()`. Rendering it as one blob printed + "tui web-ui" as if it were a single odd token, so split it here + the same way it is split there. One value stays one chip. #} + {% for v in value | string | split_facet %}{{ v }}{% endfor %} {% if key == 'namespace' and pkg.namespace_implicit %} {{ t('pkg.index_default') }} {% endif %}