fix: give realtime voice a dedicated conversation view
Rebuild & Deploy / rebuild (push) Successful in 3m7s

This commit is contained in:
snowspeeder committed 2026-10-04 10:35:34 -04:00
1 parent 257ab48f0f
commit 8905dc2c2a
9 files changed
+302 -101

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
"""Offline dedicated voice shell tests using the real start/stop code and fake SDK."""
from playwright.sync_api import sync_playwright, expect
from test_chat_web import fixture
from test_voice_settings_ui import VOICE_SDK
def transport(page, token_failure=False, connecting=False):
page.route('https://hope.test/livekit-client.umd.js', lambda r: r.fulfill(
body=VOICE_SDK.replace("this.state='connected';const agent", "if(window.holdConnect) await new Promise(resolve=>window.releaseConnect=resolve);this.state='connected';const agent"),
content_type='application/javascript'))
def token(r):
page.evaluate('(body) => window.lastTokenBody=body', r.request.post_data_json)
if token_failure:
r.fulfill(status=503, json={'error': 'Voice unavailable'})
else:
r.fulfill(json={'token': 'offline-token', 'conversation_id': r.request.post_data_json.get('conversation_id')})
page.route('https://hope.test/token', token)
if connecting:
page.add_init_script('window.holdConnect=true')
def open_thread(page, width):
page.goto('https://hope.test/'); page.wait_for_load_state('networkidle')
if width <= 760:
page.locator('#drawerOpen').click()
page.get_by_role('button', name='A saved thought', exact=True).click()
expect(page.locator('#chatTimeline')).to_contain_text('Original question')
def packet(page, text):
page.evaluate("""text => testRoom.emit('DataReceived',new TextEncoder().encode(JSON.stringify({type:'transcript',role:'agent',text})),testRoom.remoteParticipants.get('agent'),null,'transcript')""", text)
def test_voice_shell_real_start_mute_scroll_stop_and_saved_history():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
for width, height in [(1440, 900), (390, 844), (320, 568), (844, 390)]:
page = browser.new_page(viewport={'width': width, 'height': height}, reduced_motion='reduce')
errors = []; page.on('pageerror', lambda e: errors.append(str(e)))
calls, _, conversations, messages = fixture(page); transport(page)
page.add_init_script("localStorage.setItem('hope_history:https://hope.test:alice',JSON.stringify([{role:'agent',text:'Unrelated account snapshot'}]))")
open_thread(page, width)
page.locator('#primaryInput').fill('Keep my draft')
page.locator('#talkToHope').click()
page.wait_for_function('HopeVoice.state().ready')
expect(page.locator('#legacyVoice')).to_be_visible()
for selector in ['#primaryComposer', '#chatTimeline', '.chat-footnote']:
expect(page.locator(selector)).to_be_hidden()
expect(page.locator('#primaryInput')).to_be_disabled()
expect(page.locator('#voiceHistory')).to_contain_text('Original question')
assert 'Unrelated account snapshot' not in page.locator('#conversation').inner_text()
page.locator('#muteBtn').click()
expect(page.locator('#muteBtn')).to_have_text('Unmute')
expect(page.locator('#muteBtn')).to_have_attribute('aria-pressed', 'true')
page.locator('#muteBtn').click()
for n in range(12): packet(page, f'Live response {n} ' + 'Readable transcript. ' * 30)
packet(page, '<img src=x onerror=alert(1)>')
assert page.locator('#messages img').count() == 0
assert page.locator('#voiceHistory').get_by_text('Original question', exact=True).count() == 1
assert page.evaluate("""() => {const s=document.querySelector('#conversation');return s.scrollHeight>s.clientHeight && s.scrollTop>0;}""")
page.locator('#conversation').evaluate('(el) => el.scrollTop=0')
packet(page, 'Do not jump while reading earlier messages')
assert page.locator('#conversation').evaluate('(el) => el.scrollTop') == 0
for control in ['#muteBtn', '#stopBtn', '#settingsBtn']:
box = page.locator(control).bounding_box()
assert box and box['y'] >= 0 and box['y'] + box['height'] <= height
assert page.evaluate('document.documentElement.scrollWidth <= innerWidth')
page.locator('#settingsBtn').click(); expect(page.locator('#settingsDialog')).to_be_visible()
page.locator('#settingsClose').click()
messages['c1'].append(dict(id='v1', role='assistant', content='Actually saved voice reply', status='completed'))
page.locator('#stopBtn').click()
expect(page.locator('#legacyVoice')).to_be_hidden()
expect(page.locator('#chatTimeline')).to_contain_text('Actually saved voice reply')
expect(page.locator('#primaryInput')).to_have_value('Keep my draft')
expect(page.locator('#primaryInput')).to_be_enabled()
expect(page.locator('#talkToHope')).to_be_focused()
assert page.evaluate('window.lastTokenBody') == {'conversation_id': 'c1'}
assert not errors
page.close()
browser.close()
def test_compatibility_text_composer_uses_http_and_preserves_primary_draft():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(); calls, _, _, _ = fixture(page); transport(page)
open_thread(page, 1280)
page.locator('#primaryInput').fill('Existing durable draft')
page.evaluate("document.getElementById('chatInput').value='Legacy text';document.getElementById('chatComposer').requestSubmit()")
expect(page.locator('#primaryInput')).to_have_value('Existing durable draft')
assert not any(path.endswith('/messages') or path == '/token' for _, path, _ in calls)
page.locator('#primaryInput').fill('')
page.evaluate("document.getElementById('chatComposer').requestSubmit()")
expect(page.locator('#chatTimeline')).to_contain_text('Legacy text')
assert any(path.endswith('/messages') and body['content'] == 'Legacy text' for _, path, body in calls)
assert page.evaluate('window.lastTokenBody') is None
assert page.evaluate('window.testRoom') is None
browser.close()
def test_voice_failure_cancel_disconnect_and_logout_restore_text():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
for mode in ['token-failure', 'mic-failure', 'cancel', 'disconnect', 'logout']:
page = browser.new_page(viewport={'width': 390, 'height': 844})
fixture(page); transport(page, token_failure=mode == 'token-failure', connecting=mode == 'cancel')
if mode == 'mic-failure': page.add_init_script('window.denyMic=true')
open_thread(page, 390); page.locator('#primaryInput').fill('Draft survives')
page.locator('#talkToHope').click()
if mode == 'cancel':
expect(page.locator('#stopBtn')).to_be_visible()
page.locator('#stopBtn').click()
page.evaluate('window.releaseConnect?.()')
elif mode not in ['token-failure', 'mic-failure']:
page.wait_for_function('HopeVoice.state().ready')
if mode == 'disconnect': page.evaluate("testRoom.emit('Disconnected')")
else:
page.locator('#drawerOpen').click(); page.locator('#accountMenuBtn').click(); page.locator('#logoutBtn').click()
expect(page.locator('#legacyVoice')).to_be_hidden()
page.wait_for_function('!HopeVoice.state().active && !HopeVoice.state().microphone_enabled')
if mode == 'logout':
expect(page.locator('#loginGate')).to_be_visible()
expect(page.locator('#primaryInput')).to_have_value('')
expect(page.locator('#voiceHistory')).to_be_empty()
else:
expect(page.locator('#primaryComposer')).to_be_visible()
expect(page.locator('#primaryInput')).to_be_enabled()
expect(page.locator('#primaryInput')).to_have_value('Draft survives')
if mode.endswith('failure'): expect(page.locator('#chatNotice')).to_contain_text('Could not start voice')
else: expect(page.locator('#chatNotice')).not_to_contain_text('saved')
page.close()
browser.close()