"""Browser smoke test for the real editor template, with deterministic mocked APIs. No real user session, database or network service is accessed. """ from __future__ import annotations import json from pathlib import Path ROOT=Path(__file__).resolve().parents[1] REPORT=ROOT/'reports'/'v829-browser' def run(): from playwright.sync_api import sync_playwright REPORT.mkdir(parents=True,exist_ok=True) editor=(ROOT/'app/templates/editor.html').read_bytes() schema={'blocks':{ 'hero':{'name':'横幅','icon':'◆','description':'顶部展示','fields':['title'],'defaults':{'title':'全球业务'}}, 'richtext':{'name':'文字介绍','icon':'☷','fields':['title'],'defaults':{'title':'介绍'}}, 'collection':{'name':'产品列表','icon':'▦','fields':['title'],'defaults':{'title':'我们的产品'}}, 'layout':{'name':'多列布局','icon':'◫','fields':['title'],'defaults':{'title':'多列布局'}}, },'page_templates':{'standard':{'name':'标准页面'}}} calls=[] def mock(route): req=route.request path=req.url.split('example.test',1)[-1].split('?',1)[0] if path=='/studio/s1':return route.fulfill(status=200,body=editor,content_type='text/html') if path.startswith('/ui/'): file=ROOT/'app/static'/path.removeprefix('/ui/') if file.is_file(): typ='text/css' if file.suffix=='.css' else 'application/javascript' return route.fulfill(status=200,body=file.read_bytes(),content_type=typ) if path=='/api/auth/me':data={'csrf':'test-token'} elif path=='/api/sites/s1':data={'id':'s1','name':'测试网站','theme':'automotive'} elif path=='/api/sites/s1/entries':data=[{'id':'p1','title':'首页','route':'/'}] elif path=='/api/editor/blocks':data=schema elif path=='/api/sites/s1/components':data=[] elif path=='/api/sites/s1/pages/p1/studio-draft':data={'revision':0,'blocks':[{'id':'b1','type':'hero','title':'全球业务','visible':True,'style':{}}],'page_template':'standard','layout_mode':'append','entry':{'title':'首页','route':'/'}} elif path=='/api/sites/s1/pages/p1/editor-preview': return route.fulfill(status=200,body='

全球业务

',content_type='text/html') elif path.startswith('/api/factory/themes/') and path.endswith('/preview'): return route.fulfill(status=200,body='
主题示例

网页主题样例

',content_type='text/html') else:return route.fulfill(status=404,body='not mocked') calls.append(path) return route.fulfill(status=200,body=json.dumps(data,ensure_ascii=False),content_type='application/json') with sync_playwright() as p: browser=p.chromium.launch(executable_path='/usr/bin/chromium',headless=True,args=['--no-sandbox','--disable-dev-shm-usage']) page=browser.new_page(viewport={'width':1440,'height':900},device_scale_factor=1) failures=[] page.on('pageerror',lambda e:failures.append(str(e))) # Navigation to local HTTP is blocked by this runner's browser policy. # Exercise the exact editor DOM, CSS, and JS in isolated set_content instead. import re html_source=editor.decode('utf-8') html_source=re.sub(r']+src="[^"]+"[^>]*>', '',html_source) html_source=re.sub(r']+href="/ui/[^"]+"[^>]*>', '',html_source) page.set_content(html_source) page.add_style_tag(content=(ROOT/'app/static/editor.css').read_text()) page.add_script_tag(content=(ROOT/'app/static/editor-helpers.js').read_text()) page.add_script_tag(content=(ROOT/'app/static/studio-experience-v829.js').read_text()) page.evaluate("""({schema}) => { const payloads={ '/api/auth/me':{csrf:'test-token'}, '/api/sites/s1':{id:'s1',name:'测试网站',theme:'automotive'}, '/api/sites/s1/entries':[{id:'p1',title:'首页',route:'/'}], '/api/editor/blocks':schema, '/api/sites/s1/components':[], '/api/sites/s1/pages/p1/studio-draft':{revision:0,blocks:[{id:'b1',type:'hero',title:'全球业务',visible:true,style:{}}],page_template:'standard',layout_mode:'append',entry:{title:'首页',route:'/'}}, }; window.fetch=async function(url){ const path=String(url).split('?')[0]; if(path.endsWith('/editor-preview'))return {ok:true,status:200,headers:{get:()=> 'text/html'},text:async()=>'

全球业务

'}; const data=payloads[path]; return {ok:!!data,status:data?200:404,headers:{get:()=> 'application/json'},json:async()=>data||{detail:'not mocked'}}; }; }""",{'schema':schema}) script=(ROOT/'app/static/editor.js').read_text().replace("const siteId=pathParts[1],initialPage=pathParts[2];","const siteId='s1',initialPage='p1';") page.add_script_tag(content=script) page.evaluate('Studio.init()') page.locator('#pageName').get_by_text('首页').wait_for(timeout=10000) page.locator('#sfAddBlock').click() assert page.locator('.sf-palette-group').count()==4 page.locator('#paletteSearch').fill('产品') assert page.locator('.sf-palette-grid button').count()==1 assert '产品列表' in page.locator('.sf-palette-grid button').inner_text() page.locator('#paletteSearch').fill('') page.locator('#studioThemePreviewButton').click() assert page.locator('#themePreviewModal').is_visible() page.locator('#themePreviewTheme').select_option('industrial') page.locator('#themePreviewPage').select_option('product') page.get_by_role('button',name='手机',exact=True).last.click() assert 'width: 390px' in page.locator('#themePreviewFrameShell').get_attribute('style') assert '/industrial/preview?page_type=product' in page.locator('#themePreviewFrame').get_attribute('src') page.locator('#themePreviewModal').locator('button[aria-label="关闭主题预览"]').click() page.locator('#tabStyle').click() page.get_by_role('button',name='柔和浅底').click() assert page.locator('#saveStatus').inner_text()=='未保存更改' assert not page.locator('#quickUndo').is_disabled() page.locator('#quickUndo').click() assert not failures,failures page.screenshot(path=str(REPORT/'v829-studio-desktop.png'),full_page=False) page.set_viewport_size({'width':390,'height':844}) page.locator('details.studio-actions summary').click() page.locator('.sf-more-theme').click() assert page.locator('#themePreviewModal').is_visible() assert page.evaluate('document.documentElement.scrollWidth <= innerWidth + 2') page.screenshot(path=str(REPORT/'v829-studio-phone.png'),full_page=False) browser.close() (REPORT/'result.json').write_text(json.dumps({'status':'passed','browser':'Chromium, route-mocked real editor assets','desktop':'palette/theme/style/undo','mobile':'390px theme dialog, no horizontal overflow','page_errors':failures,'api_calls':calls},ensure_ascii=False,indent=2)) print('PASS: browser studio V8.29 desktop + 390px mobile. No JS errors.') if __name__=='__main__':run()