mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 19:50:15 +00:00
3f838fc31a
release: v0.50.244 Batch release of 4 PRs: - #1303 (@fecolinhares) — TTS playback of agent responses via Web Speech API. Per-message speaker button + auto-read toggle + voice/rate/pitch in Settings. localStorage-only state. Closes #499. - #1304 — Stale saved session 404 cleanup + structured api() errors. Salvaged from #1084. Independently approved on358275e. - #1306 — Cmd/Ctrl+K works while a conversation is busy. Salvaged from #1084. Independently approved on2e8a239. - #1307 — Sienna skin (warm clay & sand earth palette). Salvaged from #1084. Independently approved on5cd79c8. Tests: 3290 passed, 2 skipped, 3 xpassed, 0 failures (was 3254; +36 tests). Independently reviewed and approved by nesquena (commit47f0e0d). End-to-end trace verified the TTS flow; security audit confirmed SpeechSynthesisUtterance is plain-text-only with no XSS surface; behavioural harness confirmed _stripForTTS handles all 12 markdown-stripping cases; bounds clamping on rate/pitch verified; opt-in behavior verified.
196 lines
6.9 KiB
Python
196 lines
6.9 KiB
Python
"""
|
|
Tests for #499: TTS playback of agent responses via Web Speech API.
|
|
|
|
Verifies that TTS utility functions, speaker button rendering, and
|
|
settings controls are present in the WebUI codebase.
|
|
"""
|
|
import os
|
|
import re
|
|
|
|
STATIC_DIR = os.path.join(os.path.dirname(__file__), '..', 'static')
|
|
|
|
|
|
def _read(filename):
|
|
return open(os.path.join(STATIC_DIR, filename), encoding='utf-8').read()
|
|
|
|
|
|
class TestTtsUtilityFunctions:
|
|
"""TTS core functions exist in ui.js."""
|
|
|
|
def test_strip_for_tts_exists(self):
|
|
src = _read('ui.js')
|
|
assert 'function _stripForTTS(' in src, \
|
|
"_stripForTTS function not found in ui.js"
|
|
|
|
def test_speak_message_exists(self):
|
|
src = _read('ui.js')
|
|
assert 'function speakMessage(' in src, \
|
|
"speakMessage function not found in ui.js"
|
|
|
|
def test_stop_tts_exists(self):
|
|
src = _read('ui.js')
|
|
assert 'function stopTTS(' in src, \
|
|
"stopTTS function not found in ui.js"
|
|
|
|
def test_auto_read_exists(self):
|
|
src = _read('ui.js')
|
|
assert 'function autoReadLastAssistant(' in src, \
|
|
"autoReadLastAssistant function not found in ui.js"
|
|
|
|
def test_strip_code_blocks(self):
|
|
"""_stripForTTS must remove ``` code blocks."""
|
|
src = _read('ui.js')
|
|
assert re.search(r'_stripForTTS.*```', src, re.DOTALL), \
|
|
"_stripForTTS must handle fenced code blocks"
|
|
|
|
def test_strip_media_paths(self):
|
|
"""_stripForTTS must replace MEDIA: paths."""
|
|
src = _read('ui.js')
|
|
assert 'MEDIA:' in src and 'a file' in src, \
|
|
"_stripForTTS must replace MEDIA: paths"
|
|
|
|
def test_uses_speech_synthesis(self):
|
|
"""speakMessage must use window.speechSynthesis."""
|
|
src = _read('ui.js')
|
|
assert 'SpeechSynthesisUtterance' in src, \
|
|
"speakMessage must create SpeechSynthesisUtterance"
|
|
assert 'speechSynthesis.speak' in src, \
|
|
"speakMessage must call speechSynthesis.speak"
|
|
|
|
|
|
class TestTtsSpeakerButton:
|
|
"""Speaker button is rendered on assistant messages."""
|
|
|
|
def test_tts_button_rendered(self):
|
|
"""ttsBtn must be generated for non-user messages."""
|
|
src = _read('ui.js')
|
|
assert 'msg-tts-btn' in src, \
|
|
"TTS button class not found in ui.js"
|
|
|
|
def test_tts_button_not_on_user_messages(self):
|
|
"""ttsBtn must only be added for non-user (assistant) messages."""
|
|
src = _read('ui.js')
|
|
# Find the ttsBtn definition — it should have !isUser guard
|
|
tts_line = [l for l in src.splitlines() if 'msg-tts-btn' in l][0]
|
|
assert '!isUser' in tts_line or 'isUser' in tts_line, \
|
|
"TTS button should have user-check guard"
|
|
|
|
def test_tts_button_in_footer(self):
|
|
"""ttsBtn must be included in the msg-actions span."""
|
|
src = _read('ui.js')
|
|
# The footHtml line should include ttsBtn
|
|
foot_lines = [l for l in src.splitlines() if 'footHtml' in l and 'msg-actions' in l]
|
|
assert any('ttsBtn' in l for l in foot_lines), \
|
|
"ttsBtn not included in footHtml msg-actions"
|
|
|
|
def test_tts_button_uses_volume_icon(self):
|
|
"""Speaker button should use volume-2 icon."""
|
|
src = _read('ui.js')
|
|
tts_line = [l for l in src.splitlines() if 'msg-tts-btn' in l][0]
|
|
assert 'volume-2' in tts_line, \
|
|
"TTS button should use volume-2 icon"
|
|
|
|
|
|
class TestTtsSettings:
|
|
"""TTS settings controls exist in the HTML and are wired in panels.js."""
|
|
|
|
def test_tts_enabled_checkbox(self):
|
|
src = _read('index.html')
|
|
assert 'settingsTtsEnabled' in src, \
|
|
"TTS enabled checkbox not found in index.html"
|
|
|
|
def test_tts_auto_read_checkbox(self):
|
|
src = _read('index.html')
|
|
assert 'settingsTtsAutoRead' in src, \
|
|
"TTS auto-read checkbox not found in index.html"
|
|
|
|
def test_tts_voice_selector(self):
|
|
src = _read('index.html')
|
|
assert 'settingsTtsVoice' in src, \
|
|
"TTS voice selector not found in index.html"
|
|
|
|
def test_tts_rate_slider(self):
|
|
src = _read('index.html')
|
|
assert 'settingsTtsRate' in src, \
|
|
"TTS rate slider not found in index.html"
|
|
|
|
def test_tts_pitch_slider(self):
|
|
src = _read('index.html')
|
|
assert 'settingsTtsPitch' in src, \
|
|
"TTS pitch slider not found in index.html"
|
|
|
|
def test_tts_settings_wired_in_panels(self):
|
|
"""TTS settings must be initialized in loadSettingsPanel."""
|
|
src = _read('panels.js')
|
|
assert 'settingsTtsEnabled' in src, \
|
|
"TTS enabled setting not wired in panels.js"
|
|
assert '_applyTtsEnabled' in src, \
|
|
"_applyTtsEnabled not called in panels.js"
|
|
|
|
def test_apply_tts_enabled_function(self):
|
|
"""_applyTtsEnabled must toggle msg-tts-btn display."""
|
|
src = _read('panels.js')
|
|
assert 'function _applyTtsEnabled(' in src, \
|
|
"_applyTtsEnabled function not found in panels.js"
|
|
|
|
|
|
class TestTtsI18n:
|
|
"""TTS i18n keys exist in the English locale."""
|
|
|
|
def test_tts_listen_key(self):
|
|
src = _read('i18n.js')
|
|
assert "tts_listen:" in src, \
|
|
"tts_listen key not found in i18n.js"
|
|
|
|
def test_tts_not_supported_key(self):
|
|
src = _read('i18n.js')
|
|
assert "tts_not_supported:" in src, \
|
|
"tts_not_supported key not found in i18n.js"
|
|
|
|
def test_tts_settings_keys(self):
|
|
src = _read('i18n.js')
|
|
for key in ['settings_label_tts', 'settings_label_tts_auto_read',
|
|
'settings_label_tts_voice', 'settings_label_tts_rate',
|
|
'settings_label_tts_pitch']:
|
|
assert f"{key}:" in src, f"{key} not found in i18n.js"
|
|
|
|
|
|
class TestTtsAutoRead:
|
|
"""Auto-read is triggered after SSE done event."""
|
|
|
|
def test_auto_read_called_in_messages(self):
|
|
src = _read('messages.js')
|
|
assert 'autoReadLastAssistant' in src, \
|
|
"autoReadLastAssistant not called in messages.js"
|
|
|
|
def test_tts_pause_on_composer_focus(self):
|
|
"""Speech should pause when user focuses the composer."""
|
|
src = _read('messages.js')
|
|
assert 'speechSynthesis.pause' in src, \
|
|
"speechSynthesis.pause not called in messages.js"
|
|
assert 'speechSynthesis.resume' in src, \
|
|
"speechSynthesis.resume not called in messages.js"
|
|
|
|
|
|
class TestTtsBoot:
|
|
"""TTS enabled state is applied on page load."""
|
|
|
|
def test_apply_tts_on_boot(self):
|
|
src = _read('boot.js')
|
|
assert '_applyTtsEnabled' in src, \
|
|
"_applyTtsEnabled not called in boot.js"
|
|
|
|
|
|
class TestTtsStyles:
|
|
"""TTS CSS styles exist."""
|
|
|
|
def test_tts_button_hidden_default(self):
|
|
src = _read('style.css')
|
|
assert '.msg-tts-btn' in src, \
|
|
".msg-tts-btn CSS class not found in style.css"
|
|
|
|
def test_tts_pulse_animation(self):
|
|
src = _read('style.css')
|
|
assert 'tts-pulse' in src, \
|
|
"tts-pulse animation not found in style.css"
|