feat: port cms header/footer chrome visuals + mobile drawer into trade-pro
Task 10j: reshape the platform chrome DOM (sf-site-head/pro-header/ pro-navigation/pro-mega/pro-footer) into the cms Header.astro/Footer.astro design via a pure CSS layer in trade-pro pro.css (--pro-* token bridge + scoped 0,2,0 overrides): sticky glass header, right-aligned semibold nav with brand underline hover, white dropdown cards with desktop hover/focus reveal (>=1025px only; drawer semantics below), cms button CTA, dark four-column-style footer with hairline copyright bar, cms wrap geometry. Mobile drawer reuses the upstream V8.21.3 four-theme mechanism verbatim (commercial.css locked styles + pro.js generic [data-pro-menu]/ #pro-navigation hooks trade-pro already emits); mobile_tabs is template-driven so trade-pro gains the render block (English labels, data-mobile-open-menu opens the shared drawer). pro.html: +footer_nav-gated <h3>Quick Links</h3> (cms column heading), +mobile tabs block. No DOM restructuring elsewhere; site_design data flow (announcement/CTA/search/footer/mobile_tabs) and pro.js hooks intact. Tests: +test_trade_pro_chrome_layout (structure/hooks/EN fallback nav/ css evidence/drawer assets + safe-area/mobile_tabs publish cycle). 35 passed in theme suite; full run 364 passed + same 10 known infra fails. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -798,6 +798,112 @@ 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_chrome_layout(factory):
|
||||
# Task 10j:cms 页眉/页脚 chrome 视觉还原 + 手机端抽屉导航。
|
||||
# 设计源 D:/project/cms/runtimes/astro-site-runtime/src/components/
|
||||
# Header.astro(玻璃吸顶 + 右对齐半粗导航 + 悬停下划线/下拉卡 + 移动
|
||||
# 右滑抽屉)/ Footer.astro(深色四栏 + 版权条),10j 以纯样式层把平台
|
||||
# chrome DOM(sf-site-head/pro-header/pro-navigation/pro-footer)重塑为
|
||||
# cms 设计——平台数据流(site_design 导航/公告/CTA/页脚选项)与 pro.js
|
||||
# 交互钩子(data-pro-menu/data-pro-catalog/data-pro-mega/pro-nav-disclose)
|
||||
# 全部保留。抽屉复用上游 V8.21.3 四主题机制(commercial.css 样式段 +
|
||||
# pro.js 通用逻辑:选择器寻钩子而非主题分支,trade-pro 输出同款类名
|
||||
# 结构即自动可用);mobile_tabs 为模板驱动,10j 给 trade-pro 补上
|
||||
# 渲染块后配置即生效(下方发布前后双构建断言)。
|
||||
import re
|
||||
sid = site(factory)
|
||||
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200
|
||||
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']
|
||||
html = factory.get(f'/preview/{sid}/{bid}/').text
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
# ── 平台 chrome 结构与交互钩子保留(数据流不破坏的结构证据)──
|
||||
header = soup.select_one('header.sf-site-head.pro-header')
|
||||
assert header is not None
|
||||
assert header.select_one('.pro-wrap.pro-head-inner') is not None
|
||||
assert header.select_one('a.pro-logo[href$="/"]') is not None # /preview/ 重写根相对 href,用后缀匹配
|
||||
toggle = header.select_one('button.pro-mobile-toggle[data-pro-menu]')
|
||||
assert toggle is not None and toggle.get('aria-controls') == 'pro-navigation'
|
||||
nav = header.select_one('nav#pro-navigation.pro-navigation')
|
||||
assert nav is not None and nav.get('aria-label')
|
||||
# catalog/mega 钩子(aria-controls 对应 mega 面板 id;面板本体按 categories 条件渲染)
|
||||
catalog = header.select_one('button.pro-catalog-toggle[data-pro-catalog]')
|
||||
assert catalog is not None and catalog.get('aria-controls') == 'pro-mega-content'
|
||||
# CTA 双挂 sf-tp-btn(cms 蓝按钮形态)+ chrome.header CTA 数据位
|
||||
cta = header.select_one('a.pro-header-cta.sf-tp-btn')
|
||||
assert cta is not None and cta.get('href', '').endswith('/contact/')
|
||||
# 公告条数据位仍在(未发布 site_design 时渲染英文兜底文案)
|
||||
assert soup.select_one('.pro-utility .pro-wrap') is not None
|
||||
# ── 英文兜底导航(tp_nav):7 项默认栏目全渲染(尾部可跟 Search 链接——
|
||||
# chrome.header.show_search 数据位默认开)──
|
||||
nav_links = nav.select(':scope > a, :scope > .pro-nav-group > a')
|
||||
nav_text = [a.get_text(strip=True) for a in nav_links]
|
||||
assert nav_text[:7] == ['Home', 'Products', 'Brands', 'News', 'Cases', 'About', 'Contact']
|
||||
assert nav_text[7:] in ([], ['Search'])
|
||||
# ── 页脚:平台 DOM + cms 栏目标题(Quick Links 随 footer_nav 数据流)
|
||||
footer = soup.select_one('footer.sf-site-foot.pro-footer')
|
||||
assert footer is not None
|
||||
assert footer.select_one('.pro-footer-top') is not None
|
||||
heading = footer.select_one('.pro-footer-links h3')
|
||||
assert heading is not None and heading.get_text(strip=True) == 'Quick Links'
|
||||
footer_nav = footer.select('.pro-footer-links ul li a')
|
||||
assert len(footer_nav) == 7 and footer_nav[0].get_text(strip=True) == 'Home'
|
||||
assert footer.select_one('.pro-footer-bottom') is not None
|
||||
# ── 10j 视觉层随构建分发(pro.css:cms chrome 映射的样式证据)──
|
||||
pro_css = factory.get(f'/preview/{sid}/{bid}/assets/pro.css').text
|
||||
assert 'Task 10j' in pro_css
|
||||
# 玻璃拟态吸顶(Header.astro .site-header)
|
||||
assert re.search(r'\.sf-theme-trade-pro \.pro-header\{[^}]*position:sticky', pro_css)
|
||||
assert 'backdrop-filter:blur(10px)' in pro_css
|
||||
# 导航:cms 右对齐半粗 + 悬停下划线(.nav-links a 的 border-block-end 方案)
|
||||
assert re.search(r'\.sf-theme-trade-pro \.pro-navigation \.pro-nav-group>a[^{]*\{[^}]*border-block-end', pro_css)
|
||||
assert 'var(--sf-tp-color-border-brand)' in pro_css
|
||||
# 下拉卡(.sub-menu):白卡 + 发丝边 + radius-sm + shadow-md 的令牌引用
|
||||
assert re.search(r'\.sf-theme-trade-pro \.pro-nav-children\{[^}]*var\(--sf-tp-color-surface-default\)', pro_css)
|
||||
# 深色页脚(.site-footer 令牌桥:surface-inverse + 版权条 border-inverse)
|
||||
assert re.search(r'\.sf-theme-trade-pro \.pro-footer\{[^}]*var\(--sf-tp-color-surface-inverse\)', pro_css)
|
||||
assert 'var(--sf-tp-color-border-inverse)' in pro_css
|
||||
# 抽屉品牌化变量桥:平台 drawer 样式段消费 --pro-* 变量,10j 注入 cms 令牌
|
||||
assert '--pro-background:var(--sf-tp-color-surface-default)' in pro_css
|
||||
# ── 上游抽屉机制复用(commercial.css/pro.js 随构建分发 + 安全区适配)──
|
||||
assert 'assets/commercial.css' in html and 'assets/pro.js' in html
|
||||
commercial = factory.get(f'/preview/{sid}/{bid}/assets/commercial.css').text
|
||||
assert '.sf-mobile-drawer-backdrop' in commercial and 'sf-drawer-open' in commercial
|
||||
assert 'env(safe-area-inset-bottom' in commercial # 抽屉/底部 tabs 的安全区适配
|
||||
pro_js = factory.get(f'/preview/{sid}/{bid}/assets/pro.js').text
|
||||
assert 'sf-drawer-header' in pro_js and 'sf-drawer-extra' in pro_js and 'Escape' in pro_js
|
||||
# ── mobile_tabs:发布 site_design 前不渲染,发布后渲染(含 menu 动作开抽屉)──
|
||||
assert 'data-mobile-tabs' not in html
|
||||
import copy
|
||||
from app import site_design_v87
|
||||
config = copy.deepcopy(site_design_v87.DEFAULTS)
|
||||
config['mobile_tabs'] = {'enabled': True, 'items': [
|
||||
{'title': 'Home', 'icon': 'home', 'route': '/', 'action': 'link'},
|
||||
{'title': 'Products', 'icon': 'products', 'route': '/products/', 'action': 'link'},
|
||||
{'title': 'Menu', 'icon': 'menu', 'route': '/', 'action': 'menu'}]}
|
||||
url = f'/api/sites/{sid}/site-design'
|
||||
assert factory.put(url, json={'config': config, 'expected_revision': 0}).status_code == 200
|
||||
assert factory.post(url + '/publish', json={'expected_revision': 1}).status_code == 200
|
||||
bid2 = factory.post(f'/api/sites/{sid}/builds', json={}).json()['version']
|
||||
html2 = factory.get(f'/preview/{sid}/{bid2}/').text
|
||||
assert 'data-mobile-tabs' in html2 and 'data-mobile-open-menu' in html2
|
||||
tabs = BeautifulSoup(html2, 'html.parser').select('.sf-mobile-tabs .sf-mobile-tab')
|
||||
assert len(tabs) == 3
|
||||
assert [t.select_one('.sf-tab-label').get_text(strip=True) for t in tabs] == ['Home', 'Products', 'Menu']
|
||||
assert tabs[0].select_one('.sf-tab-icon') is not None # 图标位渲染
|
||||
# 发布后 chrome_is_custom 生效:公告条走配置数据位(announcement 文案 +
|
||||
# show_announcement 开关)。publish 会清空草稿 → 新 PUT 从 revision 0 重新开始
|
||||
announced = {**config, 'header': {**config['header'], 'announcement': 'Global sourcing update', 'show_announcement': True}}
|
||||
assert factory.put(url, json={'config': announced, 'expected_revision': 0}).status_code == 200
|
||||
assert factory.post(url + '/publish', json={'expected_revision': 1}).status_code == 200
|
||||
bid3 = factory.post(f'/api/sites/{sid}/builds', json={}).json()['version']
|
||||
html3 = factory.get(f'/preview/{sid}/{bid3}/').text
|
||||
assert 'Global sourcing update' in html3
|
||||
# 本构建链接全通(chrome 改动不产生断链)
|
||||
report = factory.get(f'/api/sites/{sid}/builds/{bid3}/report').json()
|
||||
assert not report['errors'] and not report['broken_links']
|
||||
|
||||
|
||||
def test_frontend_entries_include_trade_pro():
|
||||
from pathlib import Path
|
||||
base = Path(__file__).resolve().parent.parent
|
||||
|
||||
Reference in New Issue
Block a user