HERMES-GIELIE/tests/test_composer_chip_lightbox.py

101 lines
4.6 KiB
Python
Raw Normal View History

feat(composer): click pasted/attached image thumbnails to lightbox-zoom them When pasting screenshots into the composer (especially multiple in sequence, now possible end-to-end with hermes-webui/hermes-swift-mac PR #74) the user has no way to verify the right image attached. The 56x56 thumbnail in the chip is fine as a UI affordance but offers no detail at all. Quote from the request: When I hit Cmd+C and save an image to the clipboard and then paste the clipboard out, I want to be able to click on any one of those uploaded images that's inside the composer bar and have it zoom up like a lightbox so I can see the image in full once it's been pasted in to the composer input. The lightbox infrastructure already exists for message-attached images (static/ui.js:269 _openImgLightbox + the doc-level click delegate at :298 for .msg-media-img). This PR extends the same delegate to also fire on .attach-thumb composer chips: - Clicking the thumbnail opens the existing image lightbox with the blob URL as src and the file name as alt text. - Audio/video chips are excluded (they have their own native <audio> / <video> controls and don't render an .attach-thumb img). - SVG thumbnails (.attach-thumb attach-thumb--svg) qualify — they are images visually. - The chip's x remove button is a sibling, not an ancestor, of the thumb — closest('.attach-thumb') from the button returns null, so removing still works without lightbox interference. Also updates static/style.css: - cursor: zoom-in on .attach-thumb (was cursor: default — actively misleading). - Subtle :hover emphasis (brightness 1.05 + scale 1.04, 120ms ease) so users discover the affordance before clicking. 5 regression tests in tests/test_composer_chip_lightbox.py pinning: - delegate handles .attach-thumb on IMG elements - delegate still handles .msg-media-img (no regression) - audio/video chips do NOT render an .attach-thumb img - cursor:zoom-in declared on the .attach-thumb selector - hover emphasis rule present Browser-verified live on port 8789: - addFiles three distinct screenshot files (mimicking three Mac sequential pastes) -> 3 chips, 3 thumbs, all distinct. - Click thumb #2 -> lightbox opens with the right image, alt text matches filename. - Click x on chip #2 -> removes that chip, no lightbox. - Escape key closes lightbox. Companion PR on the Mac side: hermes-webui/hermes-swift-mac#74 (unique filename per paste so sequential pastes actually appear as distinct chips). Refs nesquena/hermes-webui#1733.
2026-05-06 19:54:04 +00:00
"""Regression tests for composer attach-thumb lightbox click behaviour.
User pasted/dropped/picked an image and wants to verify the right one
attached before sending. Clicking the thumbnail in the composer's
attach-tray should open the existing image lightbox (the same one
that's wired to message-attached images).
This file pins the wiring at the source level — the document-level
delegated click handler must:
- Continue handling .msg-media-img (existing v0.50.x behaviour).
- Also handle .attach-thumb on IMG elements (new in this PR).
- NOT trigger on the chip's × remove button (sibling element).
- NOT trigger on audio/video chips (those have native controls).
It also pins the CSS cursor affordance so users discover the feature.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
UI = ROOT / "static" / "ui.js"
STYLE = ROOT / "static" / "style.css"
class TestComposerChipLightboxDelegate:
def test_delegate_handles_attach_thumb_clicks(self):
"""The document click handler must pick up clicks on .attach-thumb
(composer image chips) and route them to _openImgLightbox().
Previously the handler only looked for .msg-media-img.
"""
src = UI.read_text(encoding="utf-8")
assert "e.target.closest('.attach-thumb')" in src, (
"Document click delegate must also match .attach-thumb"
)
# And it must call _openImgLightbox in that path.
# Use a tighter anchor block to ensure both branches are wired.
anchor = (
"img = e.target.closest('.attach-thumb');\n"
" if(img && img.tagName === 'IMG'){\n"
)
assert anchor in src
def test_delegate_still_handles_message_attached_images(self):
"""Existing .msg-media-img wiring must not regress."""
src = UI.read_text(encoding="utf-8")
# The message-image branch must come first (so _openImgLightbox
# fires for them without falling through to the .attach-thumb check).
msg_branch = "let img = e.target.closest('.msg-media-img');\n if(img){ _openImgLightbox(img.src, img.alt); return; }"
assert msg_branch in src
def test_delegate_excludes_audio_video_chips(self):
"""Audio/video chips have their own inline controls (native <audio>
/ <video>) — they don't get a thumbnail .attach-thumb at all, so
the handler can't possibly trigger on them. Pin that the chip
renderer uses .attach-chip--audio / .attach-chip--video sibling
classes (no IMG with class attach-thumb in those branches).
"""
src = UI.read_text(encoding="utf-8")
# Audio chip block — uses <audio>, no .attach-thumb img
assert "<audio controls preload=\"metadata\"" in src
# Video chip block — uses <video>, no .attach-thumb img
assert "<video controls preload=\"metadata\"" in src
# The .attach-thumb img tag is only generated in the image / svg branches.
# Quick structural check: every chip-rendering line that emits
# `class="attach-thumb"` has either `<img class="attach-thumb"` or
# `attach-thumb attach-thumb--svg`. Both are images.
for line in src.splitlines():
if 'class="attach-thumb' in line:
assert "<img " in line, (
"Every .attach-thumb emission should be an <img> tag, "
f"got: {line.strip()[:120]}"
)
class TestComposerChipCursorAffordance:
def test_attach_thumb_cursor_is_zoom_in(self):
"""`cursor: zoom-in` signals to the user that the thumbnail is
clickable for zoom — the most discoverable affordance for this UX.
Previously it was `cursor: default` which silently advertised
non-interactivity.
"""
src = STYLE.read_text(encoding="utf-8")
# The .attach-thumb rule must declare cursor:zoom-in
# Use a substring search resilient to other property additions.
for line in src.splitlines():
if line.strip().startswith(".attach-thumb{"):
assert "cursor:zoom-in" in line, (
f".attach-thumb cursor must be 'zoom-in', got: {line.strip()[:120]}"
)
break
else:
raise AssertionError(".attach-thumb selector not found in style.css")
def test_attach_thumb_has_hover_emphasis(self):
"""Subtle hover emphasis (brightness + scale) reinforces the
zoom-in cursor by giving instant visual feedback before click.
"""
src = STYLE.read_text(encoding="utf-8")
assert ".attach-thumb:hover{" in src or ".attach-thumb:hover {" in src