Files
cms-SiteForge/scripts/browser_studio_v8211.py
浅念 601451f22b upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)
Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.

New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.

compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).

Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:31:03 +08:00

70 lines
4.7 KiB
Python

"""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()