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:
@@ -798,182 +798,6 @@ def test_trade_pro_standard_layout_structure(factory):
|
||||
assert ssoup.select_one('.sf-tp-page-hero-lead') is not None # bootstrap 摘要在导语位
|
||||
|
||||
|
||||
def test_trade_pro_variant_pages_structure(factory):
|
||||
# Task 11:brand/service/faq/search/notfound 五个次要页面精修(布局移植
|
||||
# 收官)。设计参照:上游四主题 1.3.0/1.4.0 对应页面的信息架构
|
||||
# (automotive 1.4.0 brand 页最完整:横幅 + 面包屑 + 锚点按钮 + 车型
|
||||
# 网格 + 品牌故事;其余为通用信息页骨架)+ 已建立的 sf-tp 设计语言
|
||||
# (page-hero/面包屑/卡片网格/排版层/空态)。
|
||||
# 数据事实(make_context):brand kind 由 related 提供同品牌产品
|
||||
# (fields.brand == 页题);service/faq 仅页面条目上下文(正文富文本,
|
||||
# sanitize 剥离 details/summary——theme.css 的折叠样式只服务块系统注入
|
||||
# 的 custom_blocks,模板正文只能走排版层);search 为构建器生成的
|
||||
# noindex 静态页(site-runtime.js + search-index.json 的 ?q= 客户端检索,
|
||||
# 上游四主题同款机制,#sf-search/#sf-search-results/#sf-search-count
|
||||
# 三钩子是平台 JS 契约);notfound 走 mode='notfound'(noindex meta)。
|
||||
import re
|
||||
sid = site(factory)
|
||||
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200
|
||||
# ── 数据夹具:显式品牌(正文/封面)+ 2 同品牌产品 + 1 异品牌产品 +
|
||||
# 服务条目(正文/封面)+ FAQ 页(page_template 显式指定)──
|
||||
made = factory.post(f'/api/sites/{sid}/entries', json={
|
||||
'type': 'brand', 'title': 'Quanta', 'route': '/brands/quanta/',
|
||||
'cover': 'https://imgs.example/quanta-hall.jpg',
|
||||
'summary': 'German engineering brand for flow control systems.',
|
||||
'body': '<p>Quanta manufactures flow control systems for export markets.</p><h2>Heritage</h2><p>Founded as an industrial valve maker.</p>'})
|
||||
assert made.status_code in (200, 201), made.text
|
||||
batch = [
|
||||
{'type': 'product', 'title': 'Quanta Valve V1', 'route': '/product/quanta-valve-v1/',
|
||||
'summary': 'Control valve from Quanta.', 'meta': {'fields': {'brand': 'Quanta', 'category': 'Valves'}}},
|
||||
{'type': 'product', 'title': 'Quanta Pump P2', 'route': '/product/quanta-pump-p2/',
|
||||
'summary': 'Centrifugal pump from Quanta.', 'meta': {'fields': {'brand': 'Quanta'}}},
|
||||
{'type': 'product', 'title': 'Rival Actuator A9', 'route': '/product/rival-actuator/',
|
||||
'summary': 'Actuator from another brand.', 'meta': {'fields': {'brand': 'Rival'}}},
|
||||
{'type': 'service', 'title': 'Export Documentation Service', 'route': '/services/export-docs/',
|
||||
'cover': 'https://imgs.example/docs-desk.jpg',
|
||||
'summary': 'Certificates of origin, packing lists and customs paperwork.',
|
||||
'body': '<p>We prepare the complete document set for every shipment.</p><h2>What is included</h2><p>Commercial invoice, packing list and certificate of origin.</p>'},
|
||||
{'type': 'page', 'title': 'Shipping FAQ', 'route': '/faq-shipping/',
|
||||
'summary': 'Frequently asked questions about shipping and documents.',
|
||||
'body': '<p>Answers about lead times and paperwork.</p><h2>How long is the lead time?</h2><p>Standard orders ship in 15 working days.</p>',
|
||||
'meta': {'page_template': 'faq'}},
|
||||
]
|
||||
for row in batch:
|
||||
r = factory.post(f'/api/sites/{sid}/entries', json=row)
|
||||
assert r.status_code in (200, 201), r.text
|
||||
assert factory.put(f'/api/factory/sites/{sid}/theme', json={'theme': 'trade-pro', 'version': '1.0.0'}).status_code == 200
|
||||
bid = factory.post(f'/api/sites/{sid}/builds', json={}).json()['version']
|
||||
|
||||
# ── v3 契约抽验:五种页面 200 / 唯一 h1 / pro-header + pro-footer ──
|
||||
variant_urls = ['/brands/quanta/', '/services/export-docs/', '/faq-shipping/', '/search/', '/404.html']
|
||||
pages = {}
|
||||
for url in variant_urls:
|
||||
r = factory.get(f'/preview/{sid}/{bid}{url}')
|
||||
assert r.status_code == 200, (url, r.text[:200])
|
||||
soup = BeautifulSoup(r.text, 'html.parser')
|
||||
assert len(soup.find_all('h1')) == 1, url
|
||||
assert soup.select_one('.pro-header') and soup.select_one('.pro-footer'), url
|
||||
# 交互箭头一律 SVG(Task 9 契约)
|
||||
assert not re.search(r'[←→↗↘⌕▾]', ''.join(a.get_text() for a in soup.find_all('a'))), url
|
||||
pages[url] = soup
|
||||
|
||||
# ── brand:hero(面包屑 Home › Brands › 品名 + 封面底图 + 锚点按钮)+
|
||||
# 同品牌产品网格(related,异品牌不混入)+ 品牌介绍(正文存在才渲染)──
|
||||
bsoup = pages['/brands/quanta/']
|
||||
hero = bsoup.select_one('section.sf-tp-page-hero.sf-tp-brand-hero')
|
||||
assert hero is not None
|
||||
bg = hero.select_one('.sf-tp-page-hero-bg')
|
||||
assert bg is not None and 'quanta-hall.jpg' in bg.get('src', '')
|
||||
assert hero.select_one('.sf-tp-breadcrumb a[href$="/"]')
|
||||
assert hero.select_one('.sf-tp-breadcrumb a[href$="/brands/"]')
|
||||
assert hero.select_one('.sf-tp-breadcrumb-current').get_text(strip=True) == 'Quanta'
|
||||
assert hero.select_one('.sf-tp-page-hero-copy h1').get_text(strip=True) == 'Quanta'
|
||||
# hero 锚点按钮(automotive 1.4.0 同款信息架构):有产品才渲染
|
||||
assert hero.select_one('.sf-tp-page-hero-copy a.sf-tp-btn[href="#tp-brand-products"]') is not None
|
||||
# 产品网格:related(fields.brand == 页题),异品牌产品不出现在页面上
|
||||
# (groups 无稳定顺序——断言与插入顺序解耦,只验集合内容)
|
||||
grid = bsoup.select_one('.sf-tp-brand-products')
|
||||
assert grid is not None and grid.get('id') == 'tp-brand-products'
|
||||
cards = grid.select('.sf-tp-card-grid .sf-tp-card')
|
||||
assert len(cards) == 2
|
||||
card_titles = {c.select_one('h3').get_text(strip=True) for c in cards}
|
||||
assert card_titles == {'Quanta Valve V1', 'Quanta Pump P2'}
|
||||
assert 'Rival Actuator' not in grid.get_text()
|
||||
assert cards[0].select_one('.sf-tp-card-media') and cards[0].select_one('a.sf-tp-more svg')
|
||||
assert grid.select_one('.sf-tp-section-head a.sf-tp-more[href$="/products/"] svg')
|
||||
# 品牌介绍:body 存在才渲染(排版层双挂);空品牌不虚构介绍
|
||||
profile = bsoup.select_one('.sf-tp-brand-profile .sf-tp-article-body .pro-rich')
|
||||
assert profile is not None and 'Heritage' in profile.get_text()
|
||||
|
||||
# ── service:hero(面包屑 Home › Services › 标题)+ 正文排版层 + CTA 侧卡 ──
|
||||
ssoup = pages['/services/export-docs/']
|
||||
shero = ssoup.select_one('section.sf-tp-page-hero.sf-tp-service-hero')
|
||||
assert shero is not None
|
||||
sbg = shero.select_one('.sf-tp-page-hero-bg')
|
||||
assert sbg is not None and 'docs-desk.jpg' in sbg.get('src', '')
|
||||
assert shero.select_one('.sf-tp-breadcrumb a[href$="/services/"]')
|
||||
assert shero.select_one('.sf-tp-breadcrumb-current').get_text(strip=True) == 'Export Documentation Service'
|
||||
body = ssoup.select_one('.sf-tp-service-body.sf-tp-article-body .pro-rich')
|
||||
assert body is not None and 'What is included' in body.get_text()
|
||||
aside = ssoup.select_one('.sf-tp-service-aside')
|
||||
assert aside is not None
|
||||
assert aside.select_one('a.sf-tp-btn[href$="/contact/"]') is not None # CTA
|
||||
assert aside.select_one('a[href$="/services/"]') is not None # 返回服务列表
|
||||
|
||||
# ── faq:hero + 正文排版层(平台无 faq 内容类型,正文富文本承担)+
|
||||
# 引导联系卡 ──
|
||||
fsoup = pages['/faq-shipping/']
|
||||
fhero = fsoup.select_one('section.sf-tp-page-hero.sf-tp-faq-hero')
|
||||
assert fhero is not None
|
||||
assert fhero.select_one('.sf-tp-breadcrumb a[href$="/faq/"]')
|
||||
fbody = fsoup.select_one('.sf-tp-faq-body.sf-tp-article-body .pro-rich')
|
||||
assert fbody is not None and 'How long is the lead time?' in fbody.get_text()
|
||||
guide = fsoup.select_one('.sf-tp-faq-guide')
|
||||
assert guide is not None and guide.select_one('a.sf-tp-btn[href$="/contact/"]')
|
||||
|
||||
# ── search:与上游四主题同款平台机制(site-runtime.js ?q= 客户端检索
|
||||
# search-index.json),三钩子契约保留 + noindex + sf-tp 化的搜索面板 ──
|
||||
psoup = pages['/search/']
|
||||
assert 'noindex' in str(psoup.find('meta', attrs={'name': 'robots'}))
|
||||
assert psoup.select_one('main#main-content.sf-search-page')
|
||||
assert psoup.select_one('label[for="sf-search"]')
|
||||
inp = psoup.select_one('input#sf-search[type="search"]')
|
||||
assert inp is not None and inp.get('autofocus') is not None
|
||||
assert psoup.select_one('#sf-search-results') is not None
|
||||
assert psoup.select_one('#sf-search-count[aria-live="polite"]') is not None
|
||||
assert psoup.find('h1').get_text(strip=True) == 'Site Search'
|
||||
# 机制资源随构建分发:site-runtime.js 页面引用 + 检索索引含真实内容
|
||||
# (write_assets 把 search-index.json 写在 dist 根,site-runtime.js 以
|
||||
# <base href="/"> 相对解析——检索 URL 契约)
|
||||
sr = factory.get(f'/preview/{sid}/{bid}/search/')
|
||||
assert 'site-runtime.js' in sr.text
|
||||
idx = factory.get(f'/preview/{sid}/{bid}/search-index.json')
|
||||
assert idx.status_code == 200 and 'Quanta Valve V1' in idx.text
|
||||
# search/notfound 无结尾 CTA 带(pro.html 既有分支)
|
||||
assert 'pro-end-cta' not in sr.text
|
||||
|
||||
# ── notfound:品牌化 404(大标题 + 英文文案 + 双按钮 + SVG 图标)──
|
||||
nsoup = pages['/404.html']
|
||||
assert 'noindex' in str(nsoup.find('meta', attrs={'name': 'robots'}))
|
||||
nsec = nsoup.select_one('.sf-tp-notfound')
|
||||
assert nsec is not None
|
||||
assert nsec.find('h1').get_text(strip=True) == '404'
|
||||
assert nsec.select_one('.icon-circle svg') is not None # icon 宏渲染
|
||||
assert nsec.select_one('a.sf-tp-btn[href$="/"]') is not None # Back to home
|
||||
assert nsec.select_one('a[href$="/products/"]') is not None # Browse products
|
||||
assert 'pro-end-cta' not in str(nsoup)
|
||||
|
||||
# 本构建链接全通(新增面包屑/CTA/锚点不产生断链)
|
||||
report = factory.get(f'/api/sites/{sid}/builds/{bid}/report').json()
|
||||
assert not report['errors'] and not report['broken_links'] and not report['missing_local_images']
|
||||
|
||||
# ── 空数据形态:空品牌(无产品/无正文)只显空态不虚构介绍;空服务/
|
||||
# 空 FAQ 正文整段隐藏、CTA/引导保留 ──
|
||||
sid2 = site(factory)
|
||||
for row in [
|
||||
{'type': 'brand', 'title': 'Orphan', 'route': '/brands/orphan/'},
|
||||
{'type': 'service', 'title': 'Placeholder Service', 'route': '/services/placeholder/'},
|
||||
{'type': 'page', 'title': 'Bare FAQ', 'route': '/faq-bare/', 'meta': {'page_template': 'faq'}},
|
||||
]:
|
||||
r = factory.post(f'/api/sites/{sid2}/entries', json=row)
|
||||
assert r.status_code in (200, 201), r.text
|
||||
assert factory.put(f'/api/factory/sites/{sid2}/theme', json={'theme': 'trade-pro', 'version': '1.0.0'}).status_code == 200
|
||||
bid2 = factory.post(f'/api/sites/{sid2}/builds', json={}).json()['version']
|
||||
for url, absent, present in [
|
||||
('/brands/orphan/', ['.sf-tp-brand-products', '.sf-tp-brand-profile', '.sf-tp-card'], ['.sf-tp-listing-empty']),
|
||||
('/services/placeholder/', ['.sf-tp-service-body'], ['.sf-tp-service-aside']),
|
||||
('/faq-bare/', ['.sf-tp-faq-body'], ['.sf-tp-faq-guide']),
|
||||
]:
|
||||
r = factory.get(f'/preview/{sid2}/{bid2}{url}')
|
||||
assert r.status_code == 200, (url, r.text[:200])
|
||||
soup = BeautifulSoup(r.text, 'html.parser')
|
||||
assert len(soup.find_all('h1')) == 1, url
|
||||
for sel in absent:
|
||||
assert soup.select_one(sel) is None, (url, sel)
|
||||
for sel in present:
|
||||
assert soup.select_one(sel) is not None, (url, sel)
|
||||
|
||||
|
||||
def test_trade_pro_chrome_layout(factory):
|
||||
# Task 10j:cms 页眉/页脚 chrome 视觉还原 + 手机端抽屉导航。
|
||||
# 设计源 D:/project/cms/runtimes/astro-site-runtime/src/components/
|
||||
|
||||
@@ -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 '<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']
|
||||
@@ -0,0 +1,81 @@
|
||||
"""V8.34 easy Banner/card presentation settings, validation and build parity."""
|
||||
from pathlib import Path
|
||||
from bs4 import BeautifulSoup
|
||||
import pytest
|
||||
from app import blueprints
|
||||
from test_factory_v2 import factory, site
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_banner_three_predefined_layouts_and_image_fit():
|
||||
for variant in ('split','image-first','centered'):
|
||||
block=blueprints.new_block('hero',{'title':'出口车型','image':'/assets/car.png',
|
||||
'layout_variant':variant,'image_fit':'contain'})
|
||||
markup=blueprints.render_blocks([block],{'name':'Demo'},{'route':'/','type':'home'},[])
|
||||
soup=BeautifulSoup(markup,'html.parser')
|
||||
assert soup.select_one(f'.sf-hero-layout-{variant}.sf-fit-contain')
|
||||
assert soup.select_one('.sf-hero-img img').get('loading')=='eager'
|
||||
assert soup.select_one('.sf-hero-img img').get('fetchpriority')=='high'
|
||||
|
||||
|
||||
def test_collection_three_card_modes_and_product_photo_fit():
|
||||
entries=[{'route':'/products/one/','title':'HDPE 管材','summary':'专业产品',
|
||||
'type':'product','cover':'/media/hdpe.webp','meta_json':'{}'}]
|
||||
for kind in ('standard','compact','showcase'):
|
||||
block=blueprints.new_block('collection',{'card_style':kind,'image_fit':'contain'})
|
||||
html=blueprints.render_blocks([block],{}, {'route':'/','type':'home'},entries)
|
||||
assert f'sf-card-style-{kind} sf-fit-contain' in html
|
||||
assert 'data-sf-filterable="false"' in html
|
||||
assert 'hdpe.webp' in html
|
||||
|
||||
|
||||
@pytest.mark.parametrize('typ,key,value',[
|
||||
('hero','layout_variant','<script>alert(1)</script>'),
|
||||
('hero','image_fit','calc(100vw)'),
|
||||
('collection','card_style','custom'),
|
||||
('collection','image_fit','url(javascript:1)'),
|
||||
])
|
||||
def test_invalid_presentation_options_blocked(typ,key,value):
|
||||
with pytest.raises(ValueError,match='不支持的区块展示选项'):
|
||||
blueprints.validate_blocks([blueprints.new_block(typ,{key:value})])
|
||||
|
||||
|
||||
def test_legacy_blocks_preserve_default_appearance():
|
||||
old_hero={'id':'hero-legacy-123','type':'hero','title':'旧首页','image':''}
|
||||
old_list={'id':'cards-legacy-123','type':'collection','title':'旧列表','source':'product','limit':6,'columns':3}
|
||||
markup=blueprints.render_blocks([old_hero,old_list],{}, {'route':'/','type':'home'},[])
|
||||
assert 'sf-hero-layout-split' not in markup
|
||||
assert 'sf-card-style-standard' not in markup
|
||||
assert 'sf-fit-cover' not in markup
|
||||
assert 'sf-section sf-collection' in markup
|
||||
|
||||
|
||||
def test_studio_uses_plain_language_and_shared_presentation():
|
||||
script=(ROOT/'app/static/editor.js').read_text()
|
||||
stylesheet=(ROOT/'app/static/blocks.css').read_text()
|
||||
for phrase in ('Banner 排版','文字左侧 · 图片右侧','图片左侧 · 文字右侧','图片优先 · 大图展示','完整显示(不裁切)'):
|
||||
assert phrase in script
|
||||
assert 'sf-hero-layout-image-first' in stylesheet
|
||||
assert 'sf-card-style-showcase' in stylesheet
|
||||
assert 'sf-fit-contain .sf-item-img img' in stylesheet
|
||||
|
||||
|
||||
def test_real_preview_and_build_use_same_variants(factory):
|
||||
sid=site(factory)
|
||||
pages=factory.get(f'/api/sites/{sid}/entries').json()
|
||||
home=next(x for x in pages if x['route']=='/')
|
||||
blocks=[blueprints.new_block('hero',{'title':'产品供应','layout_variant':'image-first','image_fit':'contain'}),
|
||||
blueprints.new_block('collection',{'title':'最新产品','card_style':'showcase','image_fit':'contain'})]
|
||||
saved=factory.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':blocks,'page_template':'home'})
|
||||
assert saved.status_code==200,saved.text
|
||||
built=factory.post(f'/api/sites/{sid}/builds',json={})
|
||||
assert built.status_code==200,built.text
|
||||
version=built.json()['version']
|
||||
built_html=factory.get(f'/preview/{sid}/{version}/').text
|
||||
assert 'sf-hero-layout-image-first sf-fit-contain' in built_html
|
||||
assert 'sf-card-style-showcase sf-fit-contain' in built_html
|
||||
css=factory.get(f'/preview/{sid}/{version}/assets/blocks.css')
|
||||
assert css.status_code==200
|
||||
assert 'sf-card-style-showcase' in css.text
|
||||
assert not factory.get(f'/api/sites/{sid}/builds/{version}/report').json()['errors']
|
||||
Reference in New Issue
Block a user