106 lines
8.0 KiB
Python
106 lines
8.0 KiB
Python
"""Browser checks for destructive confirmations and unsafe file handling; no backend."""
|
||
import functools
|
||
import http.server
|
||
import json
|
||
from pathlib import Path
|
||
import threading
|
||
from playwright.sync_api import sync_playwright, expect
|
||
ROOT=Path(__file__).resolve().parents[1]
|
||
class Quiet(http.server.SimpleHTTPRequestHandler):
|
||
def log_message(self,*_): pass
|
||
server=http.server.ThreadingHTTPServer(('127.0.0.1',0),functools.partial(Quiet,directory=str(ROOT/'dist')))
|
||
thread=threading.Thread(target=server.serve_forever);thread.start()
|
||
url=f'http://127.0.0.1:{server.server_port}'
|
||
identity={'id':'demo-person','name':'小满','birth':'2000-01-01','hour':'亥时','relation':'朋友','nickname':'','boundary':'尊重彼此'}
|
||
state={'version':1,'identity':identity,'settings':{'city':'杭州','appearance':'reference-v1','reduced':False,'services':{'对话模型':'','ASR':'','TTS':'','视觉观察':''}},'events':[],'memories':[],'messages':[],'lastVisit':9999999999999}
|
||
checks=[]
|
||
def fileinput(page,label,data):
|
||
page.get_by_label(label,exact=True).set_input_files({'name':'demo.json','mimeType':'application/json','buffer':json.dumps(data).encode()})
|
||
def click(page,name): page.get_by_role('button',name=name,exact=True).click()
|
||
def section(page,name):page.get_by_role('navigation',name='设置分区').get_by_role('button',name=name,exact=True).click()
|
||
try:
|
||
with sync_playwright() as p:
|
||
browser=p.chromium.launch(headless=True,args=['--no-sandbox'])
|
||
ctx=browser.new_context(viewport={'width':1280,'height':800},accept_downloads=True)
|
||
ctx.add_init_script('if(!localStorage.getItem("cybergril.demo.v1")) localStorage.setItem("cybergril.demo.v1",'+json.dumps(json.dumps(state))+')')
|
||
page=ctx.new_page();page.set_default_timeout(7000)
|
||
outgoing=[];page.on('request',lambda r:outgoing.append(r.url))
|
||
page.goto(url+'/?demo=1')
|
||
page.get_by_role('navigation',name='主导航').get_by_role('button',name='设置',exact=True).click()
|
||
section(page,'本地演示导出')
|
||
label='恢复同一位她的本地演示 JSON'
|
||
other={**state,'identity':{**identity,'id':'other'}}
|
||
fileinput(page,label,other);expect(page.get_by_role('alert')).to_contain_text('另一位')
|
||
imported={**state,'settings':{**state['settings'],'city':'成都'},'messages':[{'id':'hostile','role':'companion','text':'<img src=x onerror=alert(1)>','lifeIds':[]}]}
|
||
fileinput(page,label,imported);click(page,'检查通过,继续恢复')
|
||
dialog=page.get_by_role('dialog');expect(dialog.get_by_role('button',name='确认',exact=True)).to_be_disabled()
|
||
page.get_by_label('输入“恢复”以继续',exact=True).fill('恢复')
|
||
with page.expect_download() as d:dialog.get_by_role('button',name='确认',exact=True).click()
|
||
assert d.value.suggested_filename.startswith('CyberGril-demo-')
|
||
expect(page.get_by_role('status').filter(has_text='本地演示资料已恢复')).to_be_visible()
|
||
section(page,'她');expect(page.locator('.kv')).to_contain_text('小满')
|
||
checks.append('Reject foreign identity; same-identity restore requires typed confirmation and pre-restore export')
|
||
section(page,'外观素材')
|
||
fileinput(page,'选择本地素材清单(演示 JSON)',{'manifest_version':1,'appearance_id':'<script>','clips':[1]})
|
||
expect(page.get_by_role('alert')).to_contain_text('检查没通过')
|
||
fileinput(page,'选择本地素材清单(演示 JSON)',{'manifest_version':1,'appearance_id':'new-look','clips':[{'src':'https://example.invalid/evil.js'}]})
|
||
expect(page.get_by_role('status').filter(has_text='未真实检测')).to_be_visible()
|
||
click(page,'应用演示外观标识');click(page,'取消')
|
||
expect(page.get_by_text('当前素材:reference-v1',exact=False)).to_be_visible()
|
||
click(page,'应用演示外观标识');page.get_by_role('dialog').get_by_role('button',name='确认',exact=True).click()
|
||
expect(page.get_by_text('当前素材:new-look',exact=False)).to_be_visible()
|
||
original=page.evaluate('JSON.parse(localStorage.getItem("cybergril.demo.v1")).identity')
|
||
assert original==identity
|
||
section(page,'外接服务')
|
||
page.get_by_label('对话模型地址',exact=True).fill('https://example.invalid/model')
|
||
page.locator('.svc').first.get_by_role('button',name='测试连接',exact=True).click()
|
||
expect(page.get_by_role('status').filter(has_text='未真实检测')).to_be_visible()
|
||
assert not [r for r in outgoing if not r.startswith(url)],outgoing
|
||
checks.append('Invalid manifests rejected; external media never loaded; appearance confirm/cancel preserves identity; service test has no network')
|
||
# Cancel a save by switching section while its simulated request is outstanding.
|
||
section(page,'城市');page.locator('.city-list button').filter(has_text='上海').click();click(page,'保存');section(page,'她')
|
||
page.wait_for_timeout(700)
|
||
assert page.evaluate('JSON.parse(localStorage.getItem("cybergril.demo.v1")).settings.city')=='成都'
|
||
section(page,'城市');click(page,'保存');expect(page.get_by_role('status').filter(has_text='已保存在本机演示')).to_be_visible()
|
||
assert page.evaluate('JSON.parse(localStorage.getItem("cybergril.demo.v1")).settings.services["对话模型"]')==''
|
||
checks.append('Changing section aborts outstanding save; saving city does not commit an unsaved service draft')
|
||
page.get_by_role('navigation',name='主导航').get_by_role('button',name='交谈',exact=True).click()
|
||
leave=page.get_by_role('dialog').get_by_role('button',name='放弃修改并离开',exact=True)
|
||
if leave.count(): leave.click()
|
||
click(page,'完整记录')
|
||
expect(page.locator('.log-panel')).to_contain_text('<img src=x onerror=alert(1)>')
|
||
expect(page.locator('.log-panel img')).to_have_count(0)
|
||
checks.append('Imported model text rendered as text, never as HTML')
|
||
page.get_by_role('navigation',name='主导航').get_by_role('button',name='此刻',exact=True).click()
|
||
page.locator('.demo > button').click();click(page,'长记录');page.locator('.demo > button').click()
|
||
page.get_by_role('navigation',name='主导航').get_by_role('button',name='日子',exact=True).click()
|
||
expect(page.locator('.ev')).to_have_count(18)
|
||
click(page,'看更早的日子');expect(page.locator('.ev')).to_have_count(30)
|
||
page.locator('.ev').nth(10).click()
|
||
scroll=page.locator('.list').evaluate('el=>el.scrollTop')
|
||
click(page,'在交谈里看这段');click(page,'结束交谈 · 回到日子')
|
||
assert page.locator('.list').evaluate('el=>el.scrollTop')==scroll
|
||
checks.append('Long timeline paginates 18 then 12 and retains exact scroll position after chat return')
|
||
ctx.close()
|
||
bad=browser.new_context();bad.add_init_script('localStorage.setItem("cybergril.demo.v1","{broken")');page=bad.new_page();page.goto(url+'/?demo=1')
|
||
expect(page.get_by_role('heading',name='资料暂时读不出来')).to_be_visible();expect(page.locator('.create')).to_have_count(0)
|
||
assert page.evaluate('localStorage.getItem("cybergril.demo.v1")')=='{broken'
|
||
checks.append('Corrupt persistence never falls back to overwriting first creation')
|
||
bad.close()
|
||
plain=browser.new_context(viewport={'width':1280,'height':800})
|
||
plain.add_init_script('Object.defineProperty(crypto,"randomUUID",{value:undefined})')
|
||
page=plain.new_page();page.goto(url+'/?demo=1')
|
||
page.get_by_label('她的名字',exact=True).fill('初夏')
|
||
click(page,'下一步');click(page,'下一步');click(page,'朋友');click(page,'下一步')
|
||
page.get_by_role('checkbox').check();click(page,'确认创建');click(page,'去看看她')
|
||
expect(page.locator('.who .name')).to_have_text('初夏')
|
||
page.get_by_role('navigation',name='主导航').get_by_role('button',name='交谈',exact=True).click()
|
||
page.get_by_role('textbox',name='想和她说的话').fill('HTTP 预览兼容')
|
||
click(page,'发送');expect(page.locator('.stage .line[data-role="companion"]')).to_contain_text('我听见了',timeout=7000)
|
||
checks.append('Creation and chat work when secure-context randomUUID is unavailable')
|
||
plain.close();browser.close()
|
||
finally:
|
||
server.shutdown();server.server_close();thread.join()
|
||
(ROOT.parents[0]/'docs/frontend/import-results.json').write_text(json.dumps(checks,ensure_ascii=False,indent=2))
|
||
for c in checks:print('PASS',c)
|