fix: give realtime voice a dedicated conversation view
Rebuild & Deploy / rebuild (push) Successful in 3m7s
Rebuild & Deploy / rebuild (push) Successful in 3m7s
This commit is contained in:
1 parent
257ab48f0f
commit
8905dc2c2a
9 files changed
+302
-101
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user