mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 11:10:18 +00:00
759c25655d
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.
101 lines
4.6 KiB
Python
101 lines
4.6 KiB
Python
"""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
|