"""Optional actual Chromium smoke on disposable site. Never contacts production.""" import json, os, socket, subprocess, sys, tempfile, time from pathlib import Path from urllib.request import urlopen import httpx from playwright.sync_api import sync_playwright ROOT=Path(__file__).resolve().parent.parent OUT=ROOT/'reports'/'studio-ux' OUT.mkdir(parents=True,exist_ok=True) def main(): with tempfile.TemporaryDirectory(prefix='sf-studio-ui-') as folder: with socket.socket() as s:s.bind(('127.0.0.1',0));port=s.getsockname()[1] base=f'http://127.0.0.1:{port}' env={**os.environ,'SITEFORGE_ADMIN_PASSWORD':'studio-smoke-only-password','SITEFORGE_SESSION_SECRET':'studio-smoke-only-secret-very-long','SITEFORGE_DATA_DIR':folder,'SITEFORGE_DB_FILE':str(Path(folder)/'siteforge.sqlite3'),'SITEFORGE_DATABASE_URL':'','SITEFORGE_REDIS_URL':'','SITEFORGE_COOKIE_SECURE':'false','NO_PROXY':'127.0.0.1,localhost','no_proxy':'127.0.0.1,localhost'} proc=subprocess.Popen([sys.executable,'-m','uvicorn','app.main:app','--host','127.0.0.1','--port',str(port)],cwd=ROOT,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE) try: for i in range(120): if proc.poll() is not None:raise RuntimeError('server exited: '+proc.stderr.read().decode()[-1200:]) try: with urlopen(base+'/api/ready',timeout=0.4) as response: if response.status==200:break except Exception:time.sleep(.1) else:raise RuntimeError('readiness timed out') with httpx.Client(base_url=base,trust_env=False) as client: login=client.post('/api/auth/login',json={'username':'admin','password':env['SITEFORGE_ADMIN_PASSWORD']});login.raise_for_status() csrf=client.get('/api/auth/me').json()['csrf'] site=client.post('/api/sites',json={'name':'V8.21 Studio UI 验收站点','theme':'corporate'},headers={'X-SiteForge-CSRF':csrf});site.raise_for_status();sid=site.json()['id'] entries=client.get(f'/api/sites/{sid}/entries').json();page_id=next(x['id'] for x in entries if x['route']=='/') cookie_names={c.name:c.value for c in client.cookies.jar} with sync_playwright() as play: browser=play.chromium.launch(executable_path='/usr/bin/chromium',headless=True,args=['--no-sandbox','--disable-dev-shm-usage','--no-proxy-server','--disable-gpu']) context=browser.new_context(viewport={'width':1600,'height':950},device_scale_factor=1) context.add_cookies([{'name':k,'value':v,'url':base} for k,v in cookie_names.items()]) page=context.new_page() errors=[];page.on('pageerror',lambda e:errors.append(str(e))) page.goto(base+f'/editor/{sid}/{page_id}',wait_until='domcontentloaded',timeout=20000) page.locator('#blockList .block-row').first.wait_for(timeout=12000) page.screenshot(path=str(OUT/'editor-desktop.png'),full_page=True) page.locator('#tabResponsive').click() assert page.locator('#tabResponsive').get_attribute('aria-selected')=='true' assert not page.locator('[data-inspector-section="responsive"]').is_hidden() page.locator('#tabContent').click() total=page.locator('.block-row').count() page.locator('#blockSearch').fill('not-find-anything') assert page.locator('.block-row').count()==0 page.locator('#blockSearch').fill('') assert page.locator('.block-row').count()==total page.locator('.block-row').first.locator('button[title="隐藏"]').click() assert page.locator('#saveStatus').inner_text().startswith('未保存') page.locator('#saveDraftButton').click() page.wait_for_function("document.querySelector('#saveStatus').textContent.startsWith('草稿')",timeout=12000) page.locator('#devicePhone').click() page.screenshot(path=str(OUT/'editor-phone-preview.png'),full_page=True) assert not errors,errors mobile=browser.new_context(viewport={'width':390,'height':844},device_scale_factor=1) mobile.add_cookies([{'name':k,'value':v,'url':base} for k,v in cookie_names.items()]) mp=mobile.new_page();mp.goto(base+f'/editor/{sid}/{page_id}',wait_until='domcontentloaded',timeout=20000) mp.locator('#blockCount').wait_for(timeout=12000) mp.locator('.pane-switcher button').first.click() assert 'show-structure' in mp.locator('body').get_attribute('class') mp.screenshot(path=str(OUT/'editor-mobile-ui.png'),full_page=True) mobile.close();browser.close() print(json.dumps({'result':'PASS','desktop_blocks':total,'browser_errors':errors,'screenshots':[str(x) for x in OUT.glob('*.png')]},ensure_ascii=False)) finally: proc.terminate() try:proc.wait(timeout=5) except subprocess.TimeoutExpired:proc.kill();proc.wait() if __name__=='__main__':main()