feat(plugin): voice-message mic UI in app.js — closes Phase 2.A UX gap (L8)
plugin-tests / test (push) Successful in 6s
plugin-tests / test (push) Successful in 6s
Migrates the boot.js mic-button behavior change from reverted fork commit
014b9eef into plugin/static/app.js. Replaces vanilla dictation-to-textbox
mic flow with voice-message mode: tap to record, tap again to stop → audio
File attached + sent automatically. Server-side audio_attachment_processor
in routes/transcribe.py transcribes for text-only models.
Implementation strategy:
- Own #btnMic onclick (boot.js sets it during init; plugin overrides via
MutationObserver after DOM mutations + idempotent dataset.svrntyVm flag)
- MediaRecorder with same mime-type fallbacks as the original
(webm/opus → webm → ogg/opus → mp4)
- DataTransfer to set #fileInput.files programmatically (cross-browser path)
- Dispatch 'change' so boot.js's fileInput.onchange → addFiles() runs
- Click #btnSend after 50ms tick so attachment registers before submit
Filename convention: voice-message-${timestamp}.{webm|ogg|mp4} — matches the
audio processor's name-prefix detection in _transcribe_audio_attachments.
Tests: 6 new JS-static checks in tests/unit/test_app_js.py (idempotent
guard, voice-message prefix, DataTransfer pattern, vault URL pin, mic
override). 26 tests total, all PASS.
Connection map: now 9 public API · 0 forced internal · 1 frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""Static checks on static/app.js — voice-message UI + vault panel.
|
||||
|
||||
Asserts the plugin's frontend code declares the right hooks and URLs without
|
||||
needing a browser. Runtime behavior tested manually + by upstream-drift smoke.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
APP_JS = Path(__file__).resolve().parents[2] / "static" / "app.js"
|
||||
|
||||
|
||||
def test_app_js_exists_and_nonempty():
|
||||
assert APP_JS.is_file()
|
||||
assert APP_JS.stat().st_size > 0
|
||||
|
||||
|
||||
def test_app_js_idempotent_guard():
|
||||
"""Loading twice must not re-execute — protocol Rule 4 (no surprise side effects)."""
|
||||
src = APP_JS.read_text()
|
||||
assert "__svrntyExtLoaded" in src
|
||||
assert "if (window.__svrntyExtLoaded) return" in src
|
||||
|
||||
|
||||
def test_app_js_voice_message_filename_prefix():
|
||||
"""voice-message-* prefix is the contract that the audio processor recognizes."""
|
||||
src = APP_JS.read_text()
|
||||
assert "voice-message-" in src, (
|
||||
"static/app.js must produce voice-message-* filenames so "
|
||||
"routes/transcribe.py:_transcribe_audio_attachments triggers"
|
||||
)
|
||||
|
||||
|
||||
def test_app_js_uses_data_transfer_for_file_attach():
|
||||
"""DataTransfer is the only cross-browser way to set <input type=file> programmatically."""
|
||||
src = APP_JS.read_text()
|
||||
assert "new DataTransfer()" in src
|
||||
|
||||
|
||||
def test_app_js_vault_status_url_pinned():
|
||||
"""Frontend → /api/vault/status URL must match the plugin route."""
|
||||
src = APP_JS.read_text()
|
||||
assert re.search(r"['\"`]/api/vault/status['\"`]", src), \
|
||||
"vault panel must call /api/vault/status (served by routes/vault_status.py)"
|
||||
|
||||
|
||||
def test_app_js_overrides_mic_button():
|
||||
"""Plugin owns #btnMic. Must use idempotent dataset flag to avoid double-install."""
|
||||
src = APP_JS.read_text()
|
||||
assert "btnMic" in src
|
||||
assert "svrntyVm" in src or "dataset" in src
|
||||
Reference in New Issue
Block a user