df2a183dbf
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>
103 lines
5.2 KiB
Python
103 lines
5.2 KiB
Python
"""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 '<script>' in html and '<b>' 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']
|