v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
"""Tests for #1096 — copy buttons work without HTTPS (execCommand fallback)."""
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Helpers ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def _read_js(name):
|
|
|
|
|
path = os.path.join('static', name)
|
|
|
|
|
with open(path) as f:
|
|
|
|
|
return f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_i18n():
|
|
|
|
|
return _read_js('i18n.js')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_ui():
|
|
|
|
|
return _read_js('ui.js')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── _copyText helper ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class TestCopyTextHelperExists:
|
|
|
|
|
"""_copyText() helper must exist with secure context check + execCommand fallback."""
|
|
|
|
|
|
|
|
|
|
def test_function_exists(self):
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
assert 'function _copyText(' in ui, '_copyText helper missing from ui.js'
|
|
|
|
|
|
|
|
|
|
def test_uses_secure_context_check(self):
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
assert 'isSecureContext' in ui, 'isSecureContext check missing — fallback wont trigger on HTTP'
|
|
|
|
|
|
|
|
|
|
def test_uses_exec_command_fallback(self):
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
assert 'execCommand' in ui, 'execCommand fallback missing — copy fails on HTTP'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── copyMsg() uses helper ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class TestCopyMsgUsesHelper:
|
|
|
|
|
|
|
|
|
|
def test_copy_msg_exists(self):
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
assert 'function copyMsg(' in ui
|
|
|
|
|
|
|
|
|
|
def test_copy_msg_calls_helper_not_clipboard_directly(self):
|
|
|
|
|
"""copyMsg must use _copyText, not navigator.clipboard.writeText."""
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
# Find copyMsg function body
|
|
|
|
|
m = re.search(r'function copyMsg\(', ui)
|
|
|
|
|
assert m, 'copyMsg function not found'
|
|
|
|
|
body = ui[m.start():m.start() + 600]
|
|
|
|
|
assert '_copyText(' in body, 'copyMsg must call _copyText helper'
|
|
|
|
|
assert 'navigator.clipboard.writeText' not in body, 'copyMsg must NOT call navigator.clipboard directly'
|
|
|
|
|
|
|
|
|
|
def test_copy_msg_has_i18n_error(self):
|
|
|
|
|
"""Error message must use t() for i18n, not hardcoded string."""
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
m = re.search(r'function copyMsg\(', ui)
|
|
|
|
|
body = ui[m.start():m.start() + 600]
|
|
|
|
|
assert "t('copy_failed')" in body, 'copyMsg error must use t("copy_failed") not hardcoded string'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Code block copy uses helper ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class TestCodeBlockCopyUsesHelper:
|
|
|
|
|
|
|
|
|
|
def test_add_copy_buttons_exists(self):
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
assert 'function addCopyButtons(' in ui
|
|
|
|
|
|
|
|
|
|
def test_code_copy_calls_helper(self):
|
|
|
|
|
"""addCopyButtons must use _copyText, not navigator.clipboard directly."""
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
m = re.search(r'function addCopyButtons\(', ui)
|
|
|
|
|
assert m, 'addCopyButtons function not found'
|
|
|
|
|
body = ui[m.start():m.start() + 2000]
|
|
|
|
|
assert '_copyText(' in body, 'addCopyButtons must call _copyText helper'
|
|
|
|
|
assert 'navigator.clipboard.writeText' not in body, 'addCopyButtons must NOT call navigator.clipboard directly'
|
|
|
|
|
|
|
|
|
|
def test_code_copy_has_catch_handler(self):
|
|
|
|
|
"""Code block copy must have .catch() — previously had none (silent failure)."""
|
|
|
|
|
ui = _read_ui()
|
|
|
|
|
m = re.search(r'function addCopyButtons\(', ui)
|
|
|
|
|
body = ui[m.start():m.start() + 2000]
|
|
|
|
|
assert '.catch(' in body, 'Code block copy button has no .catch() handler'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── i18n ────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
class TestCopyFailedI18n:
|
|
|
|
|
|
|
|
|
|
def test_copy_failed_in_all_locales(self):
|
2026-04-26 21:24:20 +00:00
|
|
|
"""copy_failed key must exist in all locale blocks (currently 7 with Korean)."""
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
i18n = _read_i18n()
|
|
|
|
|
count = i18n.count('copy_failed')
|
2026-04-26 21:24:20 +00:00
|
|
|
assert count >= 6, f'Expected copy_failed in at least 6 locale blocks, found {count}'
|