feat: port cms about layout into trade-pro
Task 10e: rewrite pages/about.html from the 2-row thin variant to the full cms about page design (about.astro, pixel baseline dist/hesupipe/en/about/): page hero with breadcrumb + cover backdrop + in-page anchor button, company intro two-col with derived stats bar, rich body (gated on body), advantages grid, dark mission band with gallery backdrop and glass feature cards, gallery-driven facility card grid (hidden when empty), certifications grid. Data mapping: about page = ordinary page entry; cms dict-only stats and factory copy have no platform fields, so stats reuse the 10a home derivation (products/categories/brands/cases counts, hidden when all empty) and facility cards are driven by page.cover + fields.gallery (make_context gallery, deduped, cover first); markFloatImages is not ported (sanitize strips class attributes, same conclusion as 10d) and the body renders through body_copy + the 10d sf-tp-article-body typography layer via dual classing. CSS lands in pro.css under sf-tp-about-* reusing the 10a/10b/10c components (two-col, stats, feature/card grids, section head, page hero, breadcrumb); responsive collapse comes from the existing 10a/10c media blocks, with about-specific 900/620 rules in a dedicated block (767 intentionally empty, covered by 10c page-hero). Tests: test_trade_pro_about_layout_structure drives a data-rich /about/ (PATCHed bootstrap entry with cover/summary/rich body/gallery + product and post entries) and an empty custom-route about page (meta.page_template), asserting structure, data mapping, and empty-data section hiding. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -401,6 +401,117 @@ def test_trade_pro_article_layout_structure(factory):
|
||||
assert not re.search(r'[←→↗↘⌕▾]', ''.join(a.get_text() for a in soup.find_all('a')))
|
||||
|
||||
|
||||
def test_trade_pro_about_layout_structure(factory):
|
||||
# Task 10e:about.html 移植 cms 关于页设计(about.astro,像素基准
|
||||
# dist/hesupipe/en/about/)后的结构契约。
|
||||
# 区块序列:PageHero(面包屑 + 封面底图 + 页内锚点按钮)→ 公司介绍两栏
|
||||
# (文案 + 实景图 + 悬浮数据条)→ 富文本正文(仅当 body 存在)→
|
||||
# 优势特性网格 → 深色使命带(实景底图 + 玻璃卡)→ 厂区图集卡(仅当
|
||||
# gallery 存在)→ 资质特性网格。平台事实:about 数据 = 普通页面条目
|
||||
# (title/summary/body/cover + fields.gallery),cms 词典中的 stats/
|
||||
# 厂区文案无对应字段——stats 复用 10a 首页的派生计数(产品/分类/品牌/
|
||||
# 案例),厂区卡片由 page.cover+fields.gallery 驱动,均空则整段隐藏。
|
||||
# 双构建断言:数据齐备的 /about/(bootstrap 必备栏目路由)+ 空数据的
|
||||
# 自定义路由 about 页(meta.page_template 显式指定,不绑定特定路由)。
|
||||
import re
|
||||
sid = site(factory)
|
||||
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200
|
||||
# 数据齐备的 about 条目:PATCH bootstrap 建立的 /about/(保留原路由)
|
||||
entries = factory.get(f'/api/sites/{sid}/entries').json()
|
||||
about_id = next(e['id'] for e in entries if e['route'] == '/about/')
|
||||
cover = 'https://imgs.example/factory-floor.jpg'
|
||||
shot1 = 'https://imgs.example/workshop-line.jpg'
|
||||
shot2 = 'https://imgs.example/finished-goods.jpg'
|
||||
patched = factory.patch(f'/api/sites/{sid}/entries/{about_id}', json={
|
||||
'title': 'About Us', 'summary': 'Manufacturer and export partner since day one.',
|
||||
'body': '<p>Quality control runs from raw material to loaded container.</p><h2>Our standards</h2><p>Every batch is inspected before dispatch.</p>',
|
||||
'cover': cover,
|
||||
'meta': {'fields': {'gallery': f'{shot1}; {shot2}'}}})
|
||||
assert patched.status_code == 200, patched.text
|
||||
# 3 个产品(1 品牌 1 分类)+ 1 篇文章:驱动派生 stats 与 /news/ 链接
|
||||
batch = [{'type': 'product', 'title': f'Valve Series {i}', 'summary': f'Control valve {i}.',
|
||||
'meta': {'fields': {'brand': 'Quanta', 'category': 'Valves'}}} for i in range(3)]
|
||||
batch.append({'type': 'post', 'title': 'Plant Expansion Update', 'summary': 'New extrusion lines.'})
|
||||
import io
|
||||
payload = json.dumps(batch).encode('utf-8')
|
||||
imported = factory.post(f'/api/sites/{sid}/import',
|
||||
files={'file': ('about-batch.json', io.BytesIO(payload), 'application/json')})
|
||||
assert imported.status_code == 200, imported.text[:300]
|
||||
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']
|
||||
|
||||
# ── 数据齐备 /about/:hero + 介绍两栏 + 正文 + 四个设计区块 ──
|
||||
html = factory.get(f'/preview/{sid}/{bid}/about/').text
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
assert len(soup.find_all('h1')) == 1
|
||||
hero = soup.select_one('section.sf-tp-page-hero.sf-tp-about-hero')
|
||||
assert hero is not None
|
||||
bg = hero.select_one('.sf-tp-page-hero-bg')
|
||||
assert bg is not None and 'factory-floor.jpg' in bg.get('src', '')
|
||||
# 面包屑:Home + 当前页标题(非链接)
|
||||
assert hero.select_one('.sf-tp-breadcrumb a')
|
||||
current = hero.select_one('.sf-tp-breadcrumb-current')
|
||||
assert current is not None and current.get_text(strip=True) == 'About Us'
|
||||
# hero 锚点按钮(cms PageHero button → #main 的页内对应物)
|
||||
assert hero.select_one('.sf-tp-page-hero-copy a.sf-tp-btn[href="#tp-about-intro"]')
|
||||
assert 'Manufacturer and export partner' in hero.select_one('.sf-tp-page-hero-lead').get_text()
|
||||
# 介绍两栏:文案列(摘要 + 有文章时的 /news/ 链接)+ 实景图 + 派生数据条
|
||||
intro = soup.select_one('section.sf-tp-about-intro .sf-tp-two-col')
|
||||
assert intro is not None
|
||||
assert intro.select_one('.sf-tp-copy h2')
|
||||
assert 'Manufacturer and export partner' in intro.select_one('.sf-tp-copy p').get_text()
|
||||
assert intro.select_one('.sf-tp-copy a.sf-tp-btn[href$="/news/"]')
|
||||
photo = intro.select_one('.sf-tp-photo img, .sf-tp-photo .sf-tp-photo-plain')
|
||||
assert photo is not None
|
||||
stats = intro.select('.sf-tp-stats .sf-tp-stat')
|
||||
assert len(stats) >= 2 and 'Products' in intro.select_one('.sf-tp-stats').get_text()
|
||||
# 富文本正文:body 存在才渲染(cms isRichAbout 分支的平台化)
|
||||
body = soup.select_one('.sf-tp-about-body .pro-rich')
|
||||
assert body is not None and 'Quality control' in body.get_text() and 'Our standards' in body.get_text()
|
||||
# 优势 / 使命 / 资质:静态设计内容(cms 词典区块),特性卡带 SVG 图标
|
||||
assert len(soup.select('.sf-tp-about-advantages .sf-tp-feature')) == 4
|
||||
assert all(f.select_one('.icon-circle svg') for f in soup.select('.sf-tp-about-advantages .sf-tp-feature'))
|
||||
mission = soup.select_one('section.section--dark.sf-tp-about-mission')
|
||||
assert mission is not None
|
||||
assert len(mission.select('.sf-tp-feature')) == 4
|
||||
assert mission.select_one('.sf-tp-intro')
|
||||
assert len(soup.select('.sf-tp-about-certs .sf-tp-feature')) == 4
|
||||
# 厂区图集卡:cover + fields.gallery 去重共 3 图
|
||||
facilities = soup.select_one('.sf-tp-about-facilities')
|
||||
assert facilities is not None
|
||||
cards = facilities.select('.sf-tp-about-facility')
|
||||
assert len(cards) == 3
|
||||
srcs = ' '.join(i.get('src', '') for i in facilities.select('img'))
|
||||
assert 'workshop-line.jpg' in srcs and 'finished-goods.jpg' in srcs
|
||||
# 交互箭头一律 SVG(Task 9 契约)
|
||||
assert not re.search(r'[←→↗↘⌕▾]', ''.join(a.get_text() for a in soup.find_all('a')))
|
||||
|
||||
# ── 空数据 about(自定义路由 + meta.page_template 显式指定)──
|
||||
# 无 cover/body/gallery/产品:hero 无底图、正文与图集整段隐藏、stats
|
||||
# 整段隐藏、/news/ 链接不渲染;静态设计区块保留(完整页面而非两栏摘要)。
|
||||
sid2 = site(factory)
|
||||
made = factory.post(f'/api/sites/{sid2}/entries', json={
|
||||
'type': 'page', 'title': 'Our Story', 'route': '/our-story/',
|
||||
'meta': {'page_template': 'about'}})
|
||||
assert made.status_code in (200, 201), made.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']
|
||||
bare = factory.get(f'/preview/{sid2}/{bid2}/our-story/')
|
||||
assert bare.status_code == 200, bare.text[:300]
|
||||
bsoup = BeautifulSoup(bare.text, 'html.parser')
|
||||
assert len(bsoup.find_all('h1')) == 1
|
||||
assert bsoup.select_one('section.sf-tp-about-hero') is not None
|
||||
assert bsoup.select_one('.sf-tp-page-hero-bg') is None
|
||||
assert bsoup.select_one('.sf-tp-about-body') is None
|
||||
assert bsoup.select_one('.sf-tp-about-facilities') is None
|
||||
assert bsoup.select_one('.sf-tp-stats') is None
|
||||
assert bsoup.select_one('.sf-tp-about-intro .sf-tp-copy a.sf-tp-btn[href$="/news/"]') is None
|
||||
assert bsoup.select_one('.sf-tp-copy p').get_text(strip=True) # 英文兜底文案
|
||||
assert len(bsoup.select('.sf-tp-about-advantages .sf-tp-feature')) == 4
|
||||
assert bsoup.select_one('section.section--dark.sf-tp-about-mission') is not None
|
||||
assert len(bsoup.select('.sf-tp-about-certs .sf-tp-feature')) == 4
|
||||
|
||||
|
||||
def test_frontend_entries_include_trade_pro():
|
||||
from pathlib import Path
|
||||
base = Path(__file__).resolve().parent.parent
|
||||
|
||||
Reference in New Issue
Block a user