c318829ff3
Task 10a (Phase 3 first layout task). home.html now mirrors the cms astro-site-runtime homepage section sequence: full-bleed hero (single active slide; the platform has no multi-slide config, so the carousel dots/JS are not ported), trustbar, two-col about with floating stats, soft products band (4-card grid), dark applications band (2x2 case tiles), advantages feature grid, soft news band (3-card grid), and the closing cta band. Data maps to SiteForge context (page.cover/summary, showcase, cases, stories, category/brand counts); empty sections hide instead of rendering shells. All new component classes are sf-tp-* prefixed and compose on the Task 8 base utilities in theme.css (.wrap/.section/.eyebrow/.icon-circle/.sf-tp-btn). CSS: the five cms components' scoped styles (PageHero, FeatureGrid, CardGrid, Cta, SectionHead + the inline homepage bands) are extracted into pro.css as the sf-tp-* component layer, tokens rewritten to --sf-tp-*, breakpoints 900/767/620 preserved. theme.css accordingly drops the superseded generic .cards/.card/.feature mobile baselines and the .pro-home-hero em bridge, and retargets the dark-surface ghost button hook from .cta-band to .sf-tp-cta. Reveal: cms uses data-reveal everywhere; a ~15-line inline script at the pro.html tail adds html.js + an IntersectionObserver that toggles is-visible (the reduced-motion/no-JS exemptions already live in theme.css from Task 8). Tests: test_theme_registry gains test_trade_pro_home_layout_structure (preview-render contract: sf-tp-hero, card grid, feature grid, cta, no pro-industrial-hero residue, reveal script). The distinct-markup tripwire in test_theme_pro_v3 is legitimately updated 4 -> 5 (renamed to drop the stale "four"): with trade-pro now carrying its own sf-tp-hero class, five themes yield five distinct hero signatures, which is what the test always meant to enforce. Co-Authored-By: Claude Code <noreply@anthropic.com>
131 lines
7.3 KiB
Python
131 lines
7.3 KiB
Python
"""Professional theme system integration tests use the real FastAPI and static builder."""
|
|
import io
|
|
import json
|
|
import re
|
|
import zipfile
|
|
from pathlib import Path
|
|
from bs4 import BeautifulSoup
|
|
from test_factory_v2 import factory, site, zip_theme
|
|
from app.core import THEMES
|
|
|
|
def test_complete_theme_families_have_distinct_markup(factory):
|
|
main=[]
|
|
for theme in THEMES:
|
|
for page_type in ('home','about','contact','listing','product','article','brand','standard','notfound'):
|
|
r=factory.get(f'/api/factory/themes/{theme}/preview',params={'page_type':page_type})
|
|
assert r.status_code==200,(theme,page_type,r.text[:400])
|
|
soup=BeautifulSoup(r.text,'html.parser')
|
|
assert soup.find('header',class_='pro-header')
|
|
assert soup.find('footer',class_='pro-footer')
|
|
assert len(soup.find_all('h1'))==1,(theme,page_type,len(soup.find_all('h1')))
|
|
assert 'pro-base.css' in r.text and 'pro.js' in r.text
|
|
if page_type=='product':assert soup.select_one('.pro-product-layout .pro-specs')
|
|
if page_type=='listing':assert soup.select_one('[data-pro-filter-search]')
|
|
if page_type=='article':assert soup.select_one('.pro-article-hero')
|
|
main.append(factory.get(f'/api/factory/themes/{theme}/preview?page_type=home').text)
|
|
# Every theme must carry its own hero markup signature. The legacy families
|
|
# share the pro-home-hero base class with a per-theme variant; trade-pro
|
|
# ports the cms design with its own sf-tp-hero class. Five themes, five
|
|
# distinct hero class strings ("4" was the pre-trade-pro coincidence).
|
|
def hero_classes(html):
|
|
soup=BeautifulSoup(html,'html.parser')
|
|
el=soup.find('section',class_=re.compile(r'^(pro-home-hero|sf-tp-hero)$'))
|
|
assert el is not None,'home hero section not found'
|
|
return ' '.join(el.get('class',[]))
|
|
assert len(set(hero_classes(s) for s in main))==5
|
|
|
|
|
|
def test_theme_build_derived_category_brand_search_and_seo(factory):
|
|
sid=site(factory)
|
|
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code==200
|
|
r=factory.post(f'/api/sites/{sid}/entries',json={
|
|
'type':'product','title':'Electric A','route':'/old/product-a.html',
|
|
'summary':'Long-range sample','seo_title':'Keep Me SEO','seo_description':'Original description',
|
|
'meta':{'fields':{'brand':'Quanta Motors','category':'SUV','range':'500 km'}}})
|
|
assert r.status_code==201,r.text
|
|
for theme in THEMES:
|
|
selected=factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':theme,'version':'1.0.0'})
|
|
assert selected.status_code==200,selected.text
|
|
result=factory.post(f'/api/sites/{sid}/builds',json={})
|
|
assert result.status_code==200,result.text
|
|
bid=result.json()['version']
|
|
for url in ['/','/about/','/contact/','/products/','/brands/','/news/','/cases/','/categories/suv/','/brands/quanta-motors/','/search/','/old/product-a.html']:
|
|
page=factory.get(f'/preview/{sid}/{bid}{url}')
|
|
assert page.status_code==200,(theme,url,page.text[:200])
|
|
soup=BeautifulSoup(page.text,'html.parser')
|
|
assert len(soup.find_all('h1'))==1,(theme,url)
|
|
assert soup.select_one('.pro-header') and soup.select_one('.pro-footer')
|
|
product=factory.get(f'/preview/{sid}/{bid}/old/product-a.html').text
|
|
assert '<title>Keep Me SEO</title>' in product
|
|
assert 'range' in product.lower() and '500 km' in product
|
|
assert 'https://test.example/old/product-a.html' in product
|
|
listing=factory.get(f'/preview/{sid}/{bid}/categories/suv/').text
|
|
assert 'Electric A' in listing
|
|
theme_css=factory.get(f'/preview/{sid}/{bid}/assets/pro.css')
|
|
assert theme_css.status_code==200 and len(theme_css.content)>2000
|
|
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']
|
|
|
|
|
|
def test_builtin_version_pin_is_real(factory):
|
|
sid=site(factory)
|
|
versions=factory.get('/api/factory/themes/automotive/versions').json()
|
|
assert {v['version'] for v in versions}=={'1.1.0','1.0.0','0.3.0'}
|
|
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'automotive','version':'0.3.0'}).status_code==200
|
|
b=factory.post(f'/api/sites/{sid}/builds',json={}).json()
|
|
old=factory.get(f'/preview/{sid}/{b["version"]}/').text
|
|
assert 'pro-auto-hero' not in old
|
|
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'automotive','version':'1.0.0'}).status_code==200
|
|
b=factory.post(f'/api/sites/{sid}/builds',json={}).json()
|
|
new=factory.get(f'/preview/{sid}/{b["version"]}/').text
|
|
assert 'pro-auto-hero' in new
|
|
|
|
|
|
def test_custom_theme_package_uses_trusted_layout_and_css(factory):
|
|
sid=site(factory)
|
|
raw=io.BytesIO(zip_theme('custom-v3','2.0.0'))
|
|
data=io.BytesIO()
|
|
with zipfile.ZipFile(raw) as old,zipfile.ZipFile(data,'w') as new:
|
|
for fn in old.namelist():
|
|
content=old.read(fn)
|
|
if fn=='manifest.json':
|
|
obj=json.loads(content);obj['layout_family']='industrial'
|
|
content=json.dumps(obj).encode()
|
|
if fn=='theme.css':content=b':root{--pro-accent:#dd5500} .pro-card{border-width:2px}'
|
|
new.writestr(fn,content)
|
|
install=factory.post('/api/factory/themes/install',files={'file':('theme.zip',data.getvalue(),'application/zip')})
|
|
assert install.status_code==200,install.text
|
|
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'custom-v3','version':'2.0.0'}).status_code==200
|
|
b=factory.post(f'/api/sites/{sid}/builds',json={}).json()
|
|
assert 'pro-industrial-hero' in factory.get(f'/preview/{sid}/{b["version"]}/').text
|
|
css=factory.get(f'/preview/{sid}/{b["version"]}/assets/theme-overrides.css').text
|
|
assert '--pro-accent:#dd5500' in css
|
|
pkg=factory.get('/api/factory/themes/custom-v3/package?version=2.0.0')
|
|
assert pkg.status_code==200
|
|
with zipfile.ZipFile(io.BytesIO(pkg.content)) as z:
|
|
assert {'manifest.json','theme.css','page-presets.json'}<=set(z.namelist())
|
|
|
|
|
|
def test_static_responsive_and_search_facilities(factory):
|
|
sid=site(factory)
|
|
b=factory.post(f'/api/sites/{sid}/builds',json={}).json()
|
|
bid=b['version']
|
|
css=factory.get(f'/preview/{sid}/{bid}/assets/pro-base.css').text
|
|
js=factory.get(f'/preview/{sid}/{bid}/assets/pro.js').text
|
|
assert '@media(max-width:560px)' in css
|
|
assert 'aria-expanded' in js and 'data-pro-filter-brand' in js
|
|
assert 'search-index.json' in factory.get(f'/preview/{sid}/{bid}/assets/site-runtime.js').text
|
|
assert 'noindex' in factory.get(f'/preview/{sid}/{bid}/search/').text
|
|
|
|
|
|
def test_all_fifty_six_page_templates_and_live_gallery(factory):
|
|
base=Path(__file__).resolve().parent.parent/'themes'
|
|
expected={'home','about','contact','listing','category','product','brand','article','case','service','faq','standard','search','notfound'}
|
|
for theme in THEMES:
|
|
assert {p.stem for p in (base/theme/'pages').glob('*.html')}==expected
|
|
for page_type in ('case','service','faq','category'):
|
|
r=factory.get(f'/api/factory/themes/{theme}/preview?page_type={page_type}')
|
|
assert r.status_code==200,(theme,page_type,r.text[:300])
|
|
page=factory.get('/theme-gallery')
|
|
assert page.status_code==200 and 'iframe' in page.text and 'page_type' in page.text
|