feat: port cms contact layout into trade-pro (info/form/no-fake-submit)
Task 10f. contact.html rebuilt from contact.astro + LeadForm.astro: PageHero (breadcrumb/cover bg) -> 0.85/1.25 info+form-card grid -> rich body section (body only). No-fake-submit dual form: configured HTTPS form_endpoint renders external form (name/email/message + pro-honeypot, 10b contract); unconfigured renders no <form> at all, guidance card with real mailto when footer_email exists. Contact items render only with real fields (email mailto; no fabricated phone/address), cms map band dropped (no address data source). CSS appended to pro.css with own 900/620 media blocks; new test test_trade_pro_contact_layout_structure covers both shapes. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -512,6 +512,97 @@ def test_trade_pro_about_layout_structure(factory):
|
||||
assert len(bsoup.select('.sf-tp-about-certs .sf-tp-feature')) == 4
|
||||
|
||||
|
||||
def test_trade_pro_contact_layout_structure(factory):
|
||||
# Task 10f:contact.html 移植 cms 联系页设计(contact.astro + LeadForm.astro,
|
||||
# 像素基准 dist/hesupipe/en/contact/)后的结构契约。
|
||||
# 区块序列:PageHero(面包屑 + 封面底图 + 眉题/h1/导语)→ 联系信息/
|
||||
# 表单双栏(0.85fr 信息列 + 1.25fr 灰底表单卡)→ 富文本正文(body
|
||||
# 存在才渲染)。cms 的地图带(Google 嵌入 = 公司真实地址数据)不移植
|
||||
# ——平台无地址字段,虚构地址即假数据;结尾 Cta 由 pro.html 全局
|
||||
# pro-end-cta 覆盖。
|
||||
# 无假提交双形态:配置 form_endpoint(HTTPS 双层校验——API 400 拒绝
|
||||
# 非 HTTPS + make_context 渲染层回退)→ 外部表单(name/email/message
|
||||
# + pro-honeypot 蜜罐,10b 同款契约,隐藏规则随构建分发);未配置 →
|
||||
# 不渲染任何 <form>,引导卡 CTA 兜底。联系字段有真实数据才渲染:
|
||||
# 邮箱 mailto;电话/地址无数据源绝不虚构。
|
||||
import re
|
||||
sid = site(factory)
|
||||
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200
|
||||
entries = factory.get(f'/api/sites/{sid}/entries').json()
|
||||
contact_id = next(e['id'] for e in entries if e['route'] == '/contact/')
|
||||
patched = factory.patch(f'/api/sites/{sid}/entries/{contact_id}', json={
|
||||
'title': 'Contact Us', 'summary': 'Product enquiries, export logistics and partnership requests.',
|
||||
'body': '<p>Reach the trade desk for specifications, pricing and delivery schedules.</p>',
|
||||
'cover': 'https://imgs.example/contact-hall.jpg'})
|
||||
assert patched.status_code == 200, patched.text
|
||||
# HTTPS 双层校验第一层:非 HTTPS 端点被 API 拒绝
|
||||
bad = factory.put(f'/api/sites/{sid}/contact-settings', json={
|
||||
'email': 'sales@example.org', 'form_endpoint': 'http://unsecure.example/lead'})
|
||||
assert bad.status_code == 400
|
||||
ok = factory.put(f'/api/sites/{sid}/contact-settings', json={
|
||||
'email': 'sales@example.org', 'form_endpoint': 'https://forms.example.org/leads'})
|
||||
assert ok.status_code == 200, ok.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']
|
||||
|
||||
# ── 形态一(已配置外部表单):hero + 双栏 + 表单 + 蜜罐 ──
|
||||
html = factory.get(f'/preview/{sid}/{bid}/contact/').text
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
assert len(soup.find_all('h1')) == 1
|
||||
hero = soup.select_one('section.sf-tp-page-hero.sf-tp-contact-hero')
|
||||
assert hero is not None
|
||||
bg = hero.select_one('.sf-tp-page-hero-bg')
|
||||
assert bg is not None and 'contact-hall.jpg' in bg.get('src', '')
|
||||
current = hero.select_one('.sf-tp-breadcrumb-current')
|
||||
assert current is not None and current.get_text(strip=True) == 'Contact Us'
|
||||
assert 'Product enquiries' in hero.select_one('.sf-tp-page-hero-lead').get_text()
|
||||
# 左栏:联系信息(眉题 + h2 + 真实邮箱 mailto + SVG 图标条目)
|
||||
grid = soup.select_one('.sf-tp-contact-grid')
|
||||
assert grid is not None
|
||||
info = grid.select_one('.sf-tp-contact-info[data-reveal]')
|
||||
assert info is not None and info.select_one('.eyebrow') and info.find('h2')
|
||||
assert info.select_one('.sf-tp-contact-item a[href="mailto:sales@example.org"]') is not None
|
||||
items = info.select('.sf-tp-contact-item')
|
||||
assert len(items) >= 2 and all(i.select_one('.icon-circle svg') for i in items)
|
||||
# 电话/地址无数据源 → 不渲染(绝不虚构)
|
||||
assert 'tel:' not in html
|
||||
# 右栏:外部表单(全页唯一 form)name/email/message + 隐藏蜜罐
|
||||
card = grid.select_one('.sf-tp-contact-form[data-reveal]')
|
||||
assert card is not None
|
||||
forms = soup.find_all('form')
|
||||
assert len(forms) == 1
|
||||
form = forms[0]
|
||||
assert form.get('action') == 'https://forms.example.org/leads' and form.get('method') == 'POST'
|
||||
for name in ('name', 'email', 'message'):
|
||||
assert form.select_one(f'[name={name}]') is not None, name
|
||||
honeypot = form.select_one('input.pro-honeypot')
|
||||
assert honeypot is not None and honeypot.get('aria-hidden') == 'true' and honeypot.get('tabindex') == '-1'
|
||||
# 蜜罐隐藏规则随构建分发(10b 修复轮的 .pro-honeypot CSS)
|
||||
pro_css = factory.get(f'/preview/{sid}/{bid}/assets/pro.css').text
|
||||
assert '.sf-theme-trade-pro .pro-honeypot' in pro_css and 'left:-9999px' in pro_css
|
||||
# 正文:body 存在才渲染(10d 排版层双挂)
|
||||
body = soup.select_one('.sf-tp-contact-body .pro-rich')
|
||||
assert body is not None and 'trade desk' in body.get_text()
|
||||
# 交互箭头一律 SVG(Task 9 契约)
|
||||
assert not re.search(r'[←→↗↘⌕▾]', ''.join(a.get_text() for a in soup.find_all('a')))
|
||||
|
||||
# ── 形态二(未配置):无任何 <form>,引导卡兜底 + 无虚构联系方式 ──
|
||||
sid2 = site(factory)
|
||||
assert factory.post(f'/api/factory/sites/{sid2}/bootstrap').status_code == 200
|
||||
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}/contact/')
|
||||
assert bare.status_code == 200, bare.text[:300]
|
||||
bsoup = BeautifulSoup(bare.text, 'html.parser')
|
||||
assert len(bsoup.find_all('form')) == 0 # 无假提交:未配置即不渲染任何表单
|
||||
assert 'mailto:' not in bare.text # 无邮箱数据 → 无 mailto(含页脚)
|
||||
assert 'tel:' not in bare.text # 绝不虚构电话
|
||||
card2 = bsoup.select_one('.sf-tp-contact-form')
|
||||
assert card2 is not None # 引导卡仍在(CTA 引导)
|
||||
assert card2.select_one('a.sf-tp-btn[href$="/products/"]') is not None
|
||||
assert len(bsoup.find_all('h1')) == 1
|
||||
|
||||
|
||||
def test_frontend_entries_include_trade_pro():
|
||||
from pathlib import Path
|
||||
base = Path(__file__).resolve().parent.parent
|
||||
|
||||
Reference in New Issue
Block a user