upgrade: adopt V8.34.1 (multi-image banner carousel + spec-table source modes)

Plain overlay (33 items, same-source lineage, zero patch replay).
Theme suites 35/35; full run 453 passed + 10 known Windows env fails
(suspect-module rerun confirms identical failure set).
Live: container 8.34.1-dev.1; hesupipe home leftover industrial-preset
blocks cleared via studio draft+publish flow — home now renders pure
trade-pro template (sf-section/Chinese blocks = 0).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-26 18:03:07 +08:00
parent 391d22f1a8
commit df2a183dbf
38 changed files with 1652 additions and 521 deletions
+102
View File
@@ -0,0 +1,102 @@
"""V8.34.1: safe opt-in multi-banner and practical no-code product specs."""
from pathlib import Path
import pytest
from bs4 import BeautifulSoup
from app.blueprints import new_block, validate_blocks, render_blocks, BLOCK_PROPERTY_SCHEMA
from test_factory_v2 import factory, site
ROOT=Path(__file__).resolve().parents[1]
def slides(*titles):
return [dict(title=t,subtitle='精选车型',description='图片说明',image='/media/car.webp',button='询价',href='/contact/') for t in titles]
def test_first_slide_priority_and_succeeding_lazy():
block=new_block('hero',{'slides':slides('A','B','C')})
html=render_blocks([block],{'name':'HESU'},{'route':'/','type':'home'},[])
soup=BeautifulSoup(html,'html.parser')
images=soup.select('.sf-hero-panel img')
assert len(images)==3
assert images[0]['loading']=='eager' and images[0]['fetchpriority']=='high'
assert [im['loading'] for im in images[1:]]==['lazy','lazy']
assert len(soup.select('[data-sf-slide-step]'))==2
assert soup.select_one('.sf-hero-count').text=='1 / 3'
assert [x.text for x in soup.select('.sf-hero-copy h1')]==['A']
assert [x.text for x in soup.select('.sf-hero-copy h2')]==['B','C']
def test_slides_are_opt_in_legacy_markup_unchanged():
old={'id':'legacy-banner-01','type':'hero','title':'Original','description':'Intro','image':'/media/x.jpg'}
html=render_blocks([old],{}, {'route':'/'},[])
assert 'sf-hero-slider' not in html and 'Original' in html
with_slide=new_block('hero',{'slides':slides('A')})
assert len(BeautifulSoup(render_blocks([with_slide],{}, {'route':'/about/'},[]),'html.parser').select('.sf-hero-panel'))==1
assert 'data-sf-slide-step' not in render_blocks([with_slide],{}, {'route':'/about/'},[])
@pytest.mark.parametrize('bad',[
[dict(title='<script>')]*7,
'fake',
[dict(title=55)],
[dict(title='one',evil='onerror')],
[dict(title='x'*2001)],
])
def test_slide_validator_rejects_invalid_payload(bad):
with pytest.raises(ValueError):validate_blocks([new_block('hero',{'slides':bad})])
def test_slide_text_xss_and_image_url_sanitized():
block=new_block('hero',{'slides':[dict(title='<script>evil()</script>',image='javascript:alert(1)',href='javascript:alert(1)',button='<b>hi</b>')]})
html=render_blocks([block],{}, {'route':'/'},[])
assert '<script>' not in html and 'javascript:' not in html
assert '&lt;script&gt;' in html and '&lt;b&gt;' in html
def test_product_spec_manual_merge_and_legacy():
page={'route':'/p/','type':'product','meta_json':'{"fields":{"brand":"BYD","range":"500km"}}'}
basic=new_block('attributes',{'title':'参数'})
initial=render_blocks([basic],{'schemas':{'product':[{'name':'brand','label':'品牌'}]}},page,[])
assert 'BYD' in initial
added=[{'label':'车长','value':'4780 mm'},{'label':'电池','value':'<script>alert(1)</script>'}]
manual=new_block('attributes',{'source_mode':'custom','items':added})
custom=render_blocks([manual],{},page,[])
assert '车长' in custom and 'BYD' not in custom
assert '<script>' not in custom
merged=render_blocks([new_block('attributes',{'source_mode':'merge','items':added})],{},page,[])
assert 'BYD' in merged and '车长' in merged
@pytest.mark.parametrize('mode,items',[
('evil',[]),('custom',[{'label':'A'}]),
('custom',[{'label':7,'value':'ok'}]),
('custom',[{'label':'A','value':'B','html':'evil'}]),
('custom',[{'label':'A','value':'B'}]*31),
])
def test_spec_validation_is_strict(mode,items):
with pytest.raises(ValueError):validate_blocks([new_block('attributes',{'source_mode':mode,'items':items})])
def test_editor_schema_and_no_code_actions():
assert BLOCK_PROPERTY_SCHEMA['hero']['properties']['slides']['type']=='array'
assert BLOCK_PROPERTY_SCHEMA['attributes']['properties']['items']['type']=='array'
script=(ROOT/'app/static/editor.js').read_text()
for key in ('多图 Banner(可选)','从图片素材库选择','从产品数据自动读取','产品数据 + 手动补充',"Studio.chooseMedia('slides'",'参数数值'):
assert key in script or key=='参数数值' and '数值' in script
runtime=(ROOT/'app/static/site-runtime.js').read_text()
assert 'data-sf-slide-step' in runtime and 'setInterval' not in runtime
def test_builded_slides_and_manual_specs(factory):
sid=site(factory)
pages=factory.get(f'/api/sites/{sid}/entries').json()
home=next(x for x in pages if x['route']=='/')
blocks=[new_block('hero',{'slides':slides('First','Second')}),new_block('attributes',{'title':'自定义参数','source_mode':'custom','items':[{'label':'长度','value':'1234 mm'}]})]
resp=factory.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':blocks,'page_template':'home'})
assert resp.status_code==200,resp.text
b=factory.post(f'/api/sites/{sid}/builds',json={})
assert b.status_code==200,b.text
version=b.json()['version']
rendered=factory.get(f'/preview/{sid}/{version}/').text
assert 'data-sf-hero-slider' in rendered and 'Second' in rendered and '1234 mm' in rendered
assert 'data-sf-slide-step' in rendered
report=factory.get(f'/api/sites/{sid}/builds/{version}/report').json()
assert not report['errors']