feat: port cms case layout into trade-pro
Task 10g: rewrite pages/case.html from the thin 1-line variant to the cms projects/[slug].astro design (pixel baseline dist/hesupipe/en/projects/): PageHero (breadcrumb Home > Cases > title, cover bg) + 1.2fr/0.8fr detail grid (overview block with location/industry rows + rich body via the 10d typography layer; sticky side column with main photo, 3-col gallery tiles and Results card) + Recommended Products + Other Projects (groups['case'] minus self), all empty-data sections hidden. Results points come from the open fields.results convention (semicolon/newline separated); cms dictionary fallback copy is never ported (fake data across sites). Listing case branch (10c evaluation): core form (hero + cols-3 cards + pagination) already matches cms projects list; only the hero copy voice differed, so listing.html gains a case-aware eyebrow/lead fallback (OUR PROJECTS). cms-specific tabs (no data source) and the FEATURED CASE STUDY band (HESU-static dictionary content) stay unported, documented in the template header. Adds test_trade_pro_case_layout_structure (build-flow: gallery/results structure, related-case dedup, empty-data hiding, /cases/ hero voice). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -603,6 +603,125 @@ def test_trade_pro_contact_layout_structure(factory):
|
||||
assert len(bsoup.find_all('h1')) == 1
|
||||
|
||||
|
||||
def test_trade_pro_case_layout_structure(factory):
|
||||
# Task 10g:case.html 移植 cms 案例详情设计(projects/[slug].astro,像素基准
|
||||
# dist/hesupipe/en/projects/<slug>/)后的结构契约。
|
||||
# 区块序列:PageHero(面包屑 Home › Cases › 标题 + 封面底图 + 眉题/h1/导语)
|
||||
# → detail-grid 双栏(1.2fr 文案列:Project Overview 眉题 + h2 + ● location/
|
||||
# industry 行 + 富文本正文;0.8fr sticky 侧列:主图 + 3 列相册缩略图 +
|
||||
# Results 成果卡)→ Recommended Products(产品卡,空则隐藏)→ Other
|
||||
# Projects(groups['case'] 排除自身,空则隐藏)。页尾 Cta 由 pro.html 的
|
||||
# pro-end-cta 覆盖。cms 的结构化字段(challenge/solution/results 词典兜底)
|
||||
# 平台无对应数据源:正文分段由富文本自身标题承担,成果卡由 fields.results
|
||||
# 开放字段驱动(分号/换行分隔要点),空则整段隐藏——绝不渲染词典兜底文案。
|
||||
import re
|
||||
sid = site(factory)
|
||||
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200
|
||||
made = factory.post(f'/api/sites/{sid}/entries', json={
|
||||
'type': 'case', 'title': 'City Drinking Water Project', 'route': '/case/city-water/',
|
||||
'cover': 'https://imgs.example/city-water.jpg', 'summary': 'Municipal supply network upgrade in Jakarta.',
|
||||
'body': '<p>The network demanded reliable joint performance under tropical site conditions.</p><h2>The Solution</h2><p>A complete piping system was delivered and supervised on site.</p>',
|
||||
'meta': {'fields': {'location': 'Jakarta, Indonesia', 'industry': 'Municipal water supply',
|
||||
'results': 'Delivered on schedule;Passed third-party inspection',
|
||||
'gallery': 'https://imgs.example/city-water.jpg; https://imgs.example/city-water-b.jpg; https://imgs.example/city-water-c.jpg'}}})
|
||||
assert made.status_code in (200, 201), made.text
|
||||
for i in range(2):
|
||||
r = factory.post(f'/api/sites/{sid}/entries', json={
|
||||
'type': 'case', 'title': f'Metro Water Supply {i}', 'route': f'/case/metro-{i}/',
|
||||
'summary': f'Reference project {i}.'})
|
||||
assert r.status_code in (200, 201), r.text
|
||||
# 1 个产品驱动 Recommended Products 区
|
||||
prod = factory.post(f'/api/sites/{sid}/entries', json={
|
||||
'type': 'product', 'title': 'HDPE Pipe PE100', 'summary': 'Pressure pipe for water supply.'})
|
||||
assert prod.status_code in (200, 201), prod.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']
|
||||
r = factory.get(f'/preview/{sid}/{bid}/case/city-water/')
|
||||
assert r.status_code == 200, r.text[:400]
|
||||
html = r.text
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
# hero:封面底图 + 面包屑(Home › Cases › 标题)+ 眉题 + 唯一 h1
|
||||
hero = soup.select_one('section.sf-tp-page-hero.sf-tp-case-hero')
|
||||
assert hero is not None
|
||||
bg = hero.select_one('.sf-tp-page-hero-bg')
|
||||
assert bg is not None and 'city-water.jpg' in bg.get('src', '')
|
||||
assert hero.select_one('.sf-tp-breadcrumb a[href$="/cases/"]')
|
||||
current = hero.select_one('.sf-tp-breadcrumb-current')
|
||||
assert current is not None and current.get_text(strip=True) == 'City Drinking Water Project'
|
||||
assert hero.select_one('.sf-tp-page-hero-copy[data-reveal] h1')
|
||||
assert 'Municipal supply network upgrade' in hero.select_one('.sf-tp-page-hero-lead').get_text()
|
||||
assert len(soup.find_all('h1')) == 1
|
||||
# detail 双栏:文案列(概览眉题 + h2 + ● location/industry 行 + 富文本正文)
|
||||
grid = soup.select_one('.sf-tp-case-grid')
|
||||
assert grid is not None
|
||||
copy = grid.select_one('.sf-tp-case-copy')
|
||||
assert copy is not None and 'project overview' in copy.select_one('.eyebrow').get_text().lower()
|
||||
assert copy.find('h2') is not None and 'City Drinking Water' in copy.find('h2').get_text()
|
||||
locations = copy.select('.sf-tp-case-location')
|
||||
assert len(locations) == 2 # location + industry(blueprints 的 case 字段)
|
||||
assert 'Jakarta, Indonesia' in locations[0].get_text()
|
||||
assert 'Municipal water supply' in locations[1].get_text()
|
||||
body = copy.select_one('.sf-tp-case-body .pro-rich')
|
||||
assert body is not None and 'The Solution' in body.get_text()
|
||||
# 侧列:主图 gallery[0] + 全量相册缩略图(cms 同款含首图)+ Results 成果卡
|
||||
side = grid.select_one('.sf-tp-case-side')
|
||||
assert side is not None
|
||||
photo = side.select_one('.sf-tp-photo img')
|
||||
assert photo is not None and 'city-water.jpg' in photo.get('src', '')
|
||||
thumbs = side.select('.sf-tp-case-gallery img')
|
||||
assert len(thumbs) == 3 # cover 去重 + b + c
|
||||
results = side.select_one('.sf-tp-case-results')
|
||||
assert results is not None and 'Results' in results.find('h3').get_text()
|
||||
points = results.select('li')
|
||||
assert len(points) == 2
|
||||
assert all(p.select_one('.sf-tp-case-check svg') for p in points) # ✓ 为 SVG 图标
|
||||
assert 'Passed third-party inspection' in points[1].get_text()
|
||||
# Recommended Products:产品卡(无产品整段隐藏)
|
||||
products = soup.select_one('.sf-tp-case-products')
|
||||
assert products is not None
|
||||
assert any('HDPE Pipe PE100' in c.get_text() for c in products.select('.sf-tp-card'))
|
||||
# Other Projects:相关案例去自身
|
||||
related = soup.select_one('.sf-tp-case-related')
|
||||
assert related is not None
|
||||
cards = related.select('.sf-tp-card')
|
||||
assert len(cards) == 2
|
||||
assert all('City Drinking Water' not in c.get_text() for c in cards)
|
||||
assert related.select_one('a.sf-tp-more[href$="/cases/"] svg') # View all projects → SVG 箭头
|
||||
# 交互箭头一律 SVG(Task 9 契约)
|
||||
assert not re.search(r'[←→↗↘⌕▾]', ''.join(a.get_text() for a in soup.find_all('a')))
|
||||
# 本构建的链接全通(新增 /cases/ 面包屑与相关案例路由不产生断链)
|
||||
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']
|
||||
|
||||
# ── 案例列表分支(10c listing 的 case 类走产品卡栅格;10g 补案例口径
|
||||
# hero 文案:cms projects 列表 OUR PROJECTS,非 PRODUCT CATALOGUE)──
|
||||
cases_html = factory.get(f'/preview/{sid}/{bid}/cases/').text
|
||||
assert 'OUR PROJECTS' in cases_html
|
||||
assert 'PRODUCT CATALOGUE' not in cases_html
|
||||
csoup = BeautifulSoup(cases_html, 'html.parser')
|
||||
assert len(csoup.select('.sf-tp-card-grid--listing .sf-tp-card')) == 3
|
||||
|
||||
# ── 空数据案例(无 cover/gallery/body/fields/其他案例/产品):相册、
|
||||
# 成果卡、推荐产品、相关案例整段隐藏;占位图保持版式完整 ──
|
||||
sid2 = site(factory)
|
||||
bare = factory.post(f'/api/sites/{sid2}/entries', json={
|
||||
'type': 'case', 'title': 'Reference Project', 'route': '/case/reference/'})
|
||||
assert bare.status_code in (200, 201), bare.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']
|
||||
page2 = factory.get(f'/preview/{sid2}/{bid2}/case/reference/')
|
||||
assert page2.status_code == 200, page2.text[:300]
|
||||
bsoup = BeautifulSoup(page2.text, 'html.parser')
|
||||
assert len(bsoup.find_all('h1')) == 1
|
||||
assert bsoup.select_one('.sf-tp-page-hero-bg') is None
|
||||
assert bsoup.select_one('.sf-tp-case-gallery') is None
|
||||
assert bsoup.select_one('.sf-tp-case-results') is None
|
||||
assert bsoup.select_one('.sf-tp-case-products') is None
|
||||
assert bsoup.select_one('.sf-tp-case-related') is None
|
||||
assert bsoup.select_one('.sf-tp-case-side .sf-tp-photo-plain') is not None # 无图占位
|
||||
assert bsoup.select_one('.sf-tp-case-location') is None # 字段空则行隐藏
|
||||
|
||||
|
||||
def test_frontend_entries_include_trade_pro():
|
||||
from pathlib import Path
|
||||
base = Path(__file__).resolve().parent.parent
|
||||
|
||||
Reference in New Issue
Block a user