"""IMP-51 (#79) u4 — tests for ``src.image_id_stamper``.
Covers the stamping contract called out in the Stage 2 plan :
1. ``USER_CONTENT_IMAGE_SELECTOR`` constant matches the canonical string
shared with the frontend (u3 typed client, u8 SlideCanvas).
2. ``stable_image_id`` is deterministic across calls and across runs
(same ``src`` → same id), and the ordinal suffix only appears for
occurrences > 0.
3. ``stamp_user_content_images`` is a pure no-op when ``sources`` is
empty / all-non-string (forward-compat invariant — current Phase Z
final.html has zero user-content imgs).
4. Allowlisted srcs are stamped; non-allowlisted (decorative) srcs are
left byte-for-byte unchanged.
5. Idempotent under re-stamping (the role-attr probe short-circuits).
6. Duplicate srcs in DOM order get an ordinal suffix.
7. Single-quoted ``src`` is recognized.
8. Self-closing XHTML ```` is preserved.
9. ``
`` tags without a ``src`` attribute are skipped (no crash).
All tests are pure-Python — no filesystem, no Selenium, no fixtures.
"""
from __future__ import annotations
import pytest
from src.image_id_stamper import (
IMAGE_ID_ATTR,
IMAGE_ROLE_ATTR,
IMAGE_ROLE_VALUE,
USER_CONTENT_IMAGE_SELECTOR,
build_image_overrides_style,
inject_image_overrides_style,
stable_image_id,
stamp_user_content_images,
)
# -- selector contract ------------------------------------------------------
def test_selector_matches_canonical_string():
# MUST stay verbatim in sync with the frontend mirror in
# Front/client/src/services/userOverridesApi.ts and the SlideCanvas
# query target in u8. Drift here breaks the persisted-override loop.
assert USER_CONTENT_IMAGE_SELECTOR == '.slide img[data-image-role="user-content"]'
def test_attribute_constants_match_selector_components():
assert IMAGE_ROLE_ATTR == "data-image-role"
assert IMAGE_ROLE_VALUE == "user-content"
assert IMAGE_ID_ATTR == "data-image-id"
assert IMAGE_ROLE_ATTR in USER_CONTENT_IMAGE_SELECTOR
assert f'"{IMAGE_ROLE_VALUE}"' in USER_CONTENT_IMAGE_SELECTOR
# -- stable_image_id --------------------------------------------------------
def test_stable_image_id_deterministic_same_src():
a = stable_image_id("/uploads/photo.png")
b = stable_image_id("/uploads/photo.png")
assert a == b
assert a.startswith("img-")
# sha1[:10] + "img-" prefix → fixed length.
assert len(a) == len("img-") + 10
def test_stable_image_id_differs_for_different_src():
assert stable_image_id("/a.png") != stable_image_id("/b.png")
def test_stable_image_id_ordinal_zero_has_no_suffix():
assert "-" not in stable_image_id("/x.png", ordinal=0)[4:]
@pytest.mark.parametrize("ordinal", [1, 2, 7])
def test_stable_image_id_ordinal_suffix(ordinal):
base = stable_image_id("/x.png", ordinal=0)
suffixed = stable_image_id("/x.png", ordinal=ordinal)
assert suffixed == f"{base}-{ordinal}"
def test_stable_image_id_rejects_non_string_src():
with pytest.raises(TypeError):
stable_image_id(None) # type: ignore[arg-type]
def test_stable_image_id_rejects_negative_ordinal():
with pytest.raises(ValueError):
stable_image_id("/x.png", ordinal=-1)
# -- stamp_user_content_images : forward-compat no-op -----------------------
def test_stamp_no_sources_is_pure_noop():
html = '
'
out, ids = stamp_user_content_images(html, sources=[None, 123, ""]) # type: ignore[list-item]
assert out == html
assert ids == []
def test_stamp_empty_html_is_safe():
out, ids = stamp_user_content_images("", sources=["/x.png"])
assert out == ""
assert ids == []
# -- stamp_user_content_images : allowlist semantics ------------------------
def test_stamp_user_content_src_stamps_role_and_id():
html = ''
out, ids = stamp_user_content_images(html, sources=["/u/p.png"])
expected_id = stable_image_id("/u/p.png")
assert ids == [expected_id]
assert IMAGE_ROLE_ATTR in out
assert f'{IMAGE_ROLE_ATTR}="{IMAGE_ROLE_VALUE}"' in out
assert f'{IMAGE_ID_ATTR}="{expected_id}"' in out
# Original attribute (alt) preserved.
assert 'alt="photo"' in out
def test_stamp_decorative_src_left_unchanged():
html = (
''
)
out, ids = stamp_user_content_images(html, sources=["/u/photo.png"])
# decorative img untouched (no data-image-role injected on it)
assert '
' in out
# user-content img stamped
assert ids == [stable_image_id("/u/photo.png")]
# decorative bg.png must not appear with the role attr
assert '/figma/bg.png' in out
decorative_segment = out.split('