133 lines
8.2 KiB
Python
133 lines
8.2 KiB
Python
"""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()
|