Files
cms-SiteForge/scripts/browser_studio_v829.py
T

112 lines
7.3 KiB
Python
Raw Normal View History

"""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='<html><body><section data-block="b1"><h1>全球业务</h1></section></body></html>',content_type='text/html')
elif path.startswith('/api/factory/themes/') and path.endswith('/preview'):
return route.fulfill(status=200,body='<html><body><header>主题示例</header><main><h1>网页主题样例</h1></main></body></html>',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'<script[^>]+src="[^"]+"[^>]*></script>', '',html_source)
html_source=re.sub(r'<link[^>]+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()=>'<html><body><h1>全球业务</h1></body></html>'};
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()