"""IMP-56 (#90) u9 — Step 13 ``text_path_stamper`` wiring tests. Verifies that :func:`src.phase_z2_pipeline.render_slide` stamps each rendered ``text-line`` opening tag with ``data-text-path="{slot_key}.{line_index}"`` via the u8 stamper (``src.text_path_stamper.stamp_zone_html``). This is the wiring unit companion to the u8 module-level tests at ``tests/test_text_path_stamper.py``. Coverage axes (Stage 2 plan u9 + Stage 1 binding contract) : - happy path : real ``bim_current_problems_paired`` template emits ``text-line`` divs for list-valued slots; the stamper attaches ``data-text-path`` with matching ``{slot_key}.{line_index}``. - non-list slots skipped : ``title`` / ``row_*_left_label`` (scalars) do NOT receive ``data-text-path`` attributes. - empty list slots emit no stamps : rows whose body list is empty contribute zero stamps. - deterministic : repeated calls produce byte-identical HTML (no nondeterministic mutation of ``slot_payload`` between renders). - empty zone : the ``__empty__`` template_id short-circuit emits no ``data-text-path`` (the stamper short-circuits on empty stamps). Fully synthetic slot_payload — no real Phase Z run, no ``v4_full32_result.yaml`` dependency. Uses the real ``bim_current_problems_paired`` family template only to exercise the genuine Jinja2 + slide_base render path. """ from __future__ import annotations import re import pytest from src.phase_z2_pipeline import render_slide # ─── Fixture helpers ──────────────────────────────────────────────── def _layout_css() -> dict: """Minimal valid layout_css for a single-zone slide. Mirrors tests/phase_z2/test_slide_base_embedded_mode.py shape. """ return {"areas": '"primary"', "cols": "1fr", "rows": "1fr"} def _paired_slot_payload( *, left_lines: list[str] | None = None, right_lines: list[str] | None = None, ) -> dict: """Build a slot_payload for the bim_current_problems_paired family. Only row_1 is populated by default; rows 2-4 stay empty so the template short-circuits to zero text-line divs for them. """ left_lines = left_lines if left_lines is not None else ["L1a", "L1b"] right_lines = right_lines if right_lines is not None else ["R1a"] payload: dict = { "title": "Synthetic Title", "row_1_left_label": "left pill 1", "row_1_left_body": [{"text": t, "indent": 0} for t in left_lines], "row_1_right_label": "right pill 1", "row_1_right_body": [{"text": t, "indent": 0} for t in right_lines], } for r in (2, 3, 4): payload[f"row_{r}_left_label"] = f"left pill {r}" payload[f"row_{r}_left_body"] = [] payload[f"row_{r}_right_label"] = f"right pill {r}" payload[f"row_{r}_right_body"] = [] return payload def _zone(template_id: str, slot_payload: dict) -> dict: return { "position": "primary", "template_id": template_id, "slot_payload": slot_payload, } def _render(zones: list[dict]) -> str: return render_slide( slide_title="t", slide_footer=None, zones_data=zones, layout_preset="single", layout_css=_layout_css(), gap_px=14, embedded_mode="embedded", ) # ─── Case 1 : happy path — list-valued slots stamped ───────────────── def test_render_slide_stamps_text_path_per_line(): """Each list-valued slot line gets data-text-path={slot}.{index}.""" payload = _paired_slot_payload( left_lines=["left line A", "left line B"], right_lines=["right line A"], ) html = _render([_zone("bim_current_problems_paired", payload)]) # left body 2 lines + right body 1 line = 3 stamps in row 1. assert 'data-text-path="row_1_left_body.0"' in html assert 'data-text-path="row_1_left_body.1"' in html assert 'data-text-path="row_1_right_body.0"' in html # row 2-4 are empty → no stamps for those slot_keys. assert "row_2_left_body" not in html assert "row_3_right_body" not in html def test_stamps_preserve_class_attribute(): """data-text-path injected before existing class attribute, both present.""" payload = _paired_slot_payload(left_lines=["only left"], right_lines=[]) html = _render([_zone("bim_current_problems_paired", payload)]) # The original class="text-line..." must survive verbatim alongside # the injected data-text-path attribute on the same opening tag. assert re.search( r'