8cea7dee20
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests) - Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR imports keep post/product types instead of collapsing to page - Docs: trade-pro theme port spec (R3) and implementation plan Co-Authored-By: Claude Code <noreply@anthropic.com>
179 lines
9.3 KiB
Python
179 lines
9.3 KiB
Python
"""P0-P4 integration regression: responsive design and staged template changes."""
|
|
import json
|
|
import sqlite3
|
|
from fastapi import HTTPException
|
|
import pytest
|
|
from app import blueprints, responsive, db, builder, v8_templates, site_design_v87
|
|
from app.core import save_entry
|
|
from tests.test_factory import client, create
|
|
|
|
|
|
def layout(**opts):
|
|
return blueprints.new_block('layout',{
|
|
'columns':[{'width':2,'mobile_order':2,'tablet_span':2,'mobile_span':1,
|
|
'blocks':[blueprints.new_block('richtext',{'title':'COLUMN_A'})]},
|
|
{'width':1,'mobile_order':1,'hide_tablet':True,'blocks':[]}],
|
|
'tablet_columns':2,'mobile_columns':2,**opts})
|
|
|
|
|
|
def home(c,sid):
|
|
return next(r for r in c.get(f'/api/sites/{sid}/entries').json() if r['route']=='/')
|
|
|
|
|
|
def test_different_device_grid_rules_and_html_data_attributes():
|
|
blocks=blueprints.validate_blocks([layout()]);css=responsive.editor_css(blocks)
|
|
assert 'repeat(2,minmax(0,1fr))!important' in css
|
|
assert 'grid-column:span 2' in css
|
|
assert 'order:2' in css and 'order:1' in css
|
|
assert 'display:none!important' in css
|
|
out=blueprints.render_blocks(blocks,{}, {'title':'Home'}, [])
|
|
assert f'data-layout="{blocks[0]["id"]}"' in out
|
|
assert 'data-col="1"' in out and 'data-col="2"' in out
|
|
assert 'COLUMN_A' in out
|
|
|
|
|
|
@pytest.mark.parametrize('bad',[{'mobile_columns':3},{'tablet_columns':0},
|
|
{'columns':[{'width':True,'blocks':[]}]},
|
|
{'columns':[{'width':1,'mobile_order':'calc(1) !important','blocks':[]}]},
|
|
{'columns':[{'width':1,'mobile_span':5,'blocks':[]}]},
|
|
{'columns':[{'width':1,'hide_mobile':'yes','blocks':[]}]},
|
|
{'style':{'mobile_padding':'expression(1)'}},
|
|
{'style':{'background':'url(javascript:evil)'}}])
|
|
def test_bad_device_config_rejected(bad):
|
|
block=layout()
|
|
block.update(bad)
|
|
with pytest.raises(ValueError):blueprints.validate_blocks([block])
|
|
|
|
|
|
def test_per_page_responsive_css_is_scoped_and_reusable_ids(client):
|
|
sid=create(client)
|
|
a=layout(); b=json.loads(json.dumps(a))
|
|
a['columns'][0]['hide_mobile']=True
|
|
b['columns'][0]['hide_mobile']=False
|
|
eid=home(client,sid)['id']
|
|
first=db.one('SELECT meta_json FROM entries WHERE site_id=? AND id=?',(sid,eid))
|
|
meta=json.loads(first['meta_json']);meta['blocks']=[a];meta['layout_mode']='replace'
|
|
db.execute('UPDATE entries SET meta_json=? WHERE id=?',(json.dumps(meta),eid))
|
|
save_entry(sid,{'type':'page','title':'About','route':'/about/',
|
|
'meta':{'blocks':[b],'layout_mode':'replace'}})
|
|
result=builder.build(sid,False)
|
|
dist=db.DATA/'builds'/sid/result['version']/'dist'
|
|
css=(dist/'assets/custom.css').read_text()
|
|
key_home=responsive.route_key('/');key_about=responsive.route_key('/about/')
|
|
assert f'body[data-siteforge-route="{key_home}"]' in css
|
|
assert f'body[data-siteforge-route="{key_about}"]' in css
|
|
assert f'data-siteforge-route="{key_home}"' in (dist/'index.html').read_text()
|
|
assert f'data-siteforge-route="{key_about}"' in (dist/'about/index.html').read_text()
|
|
assert css.count('display:none!important')>=1
|
|
|
|
|
|
def test_responsive_structure_survives_snapshot_and_studio_publish(client):
|
|
sid=create(client);eid=home(client,sid)['id']
|
|
data={'blocks':[layout()], 'page_template':'home','layout_mode':'replace','expected_revision':0}
|
|
url=f'/api/sites/{sid}/pages/{eid}/studio-draft'
|
|
assert client.put(url,json=data).status_code==200
|
|
assert 'COLUMN_A' not in (db.DATA/'builds'/sid/builder.build(sid,False)['version']/'dist/index.html').read_text()
|
|
assert client.post(url+'/publish',json={'expected_revision':1}).status_code==200
|
|
doc=v8_templates.export_template(sid,'copy')
|
|
assert doc['pages'][0]['blocks'][0]['mobile_columns']==2
|
|
assert doc['pages'][0]['blocks'][0]['columns'][0]['mobile_order']==2
|
|
assert 'COLUMN_A' in (db.DATA/'builds'/sid/builder.build(sid,False)['version']/'dist/index.html').read_text()
|
|
|
|
|
|
def test_stage_requires_owner_publish_and_does_not_touch_live(client):
|
|
sid=create(client)
|
|
doc=v8_templates.export_template(sid,'site template')
|
|
doc['tokens']['accent']='#123abc'
|
|
before=db.one('SELECT accent,settings_json FROM sites WHERE id=?',(sid,))
|
|
stage=client.post(f'/api/factory/sites/{sid}/whole-site-templates/apply',json={
|
|
'template':doc,'confirm':True,'overwrite_layouts':True})
|
|
assert stage.status_code==200,stage.text
|
|
assert stage.json()['published_unchanged']
|
|
assert db.one('SELECT accent,settings_json FROM sites WHERE id=?',(sid,))==before
|
|
pending=client.get(f'/api/factory/sites/{sid}/whole-site-templates/staged/current').json()
|
|
assert pending['state']=='draft' and pending['conflict'] is False
|
|
applied=client.post(f'/api/factory/sites/{sid}/whole-site-templates/staged/publish',json={
|
|
'expected_revision':pending['revision']})
|
|
assert applied.status_code==200,applied.text
|
|
assert applied.json()['transactional'] is True
|
|
assert db.one('SELECT accent FROM sites WHERE id=?',(sid,))['accent']=='#123abc'
|
|
assert client.get(f'/api/factory/sites/{sid}/whole-site-templates/staged/current').json()['state']=='none'
|
|
|
|
|
|
def test_stale_stage_does_not_overwrite_changed_site(client):
|
|
sid=create(client)
|
|
doc=v8_templates.export_template(sid,'Template')
|
|
url=f'/api/factory/sites/{sid}/whole-site-templates'
|
|
assert client.post(url+'/apply',json={'template':doc,'confirm':True,'overwrite_layouts':True}).status_code==200
|
|
db.execute('UPDATE sites SET accent=? WHERE id=?',('#ff0000',sid))
|
|
result=client.post(url+'/staged/publish',json={'expected_revision':1})
|
|
assert result.status_code==409,result.text
|
|
assert db.one('SELECT accent FROM sites WHERE id=?',(sid,))['accent']=='#ff0000'
|
|
assert client.delete(url+'/staged/current').status_code==200
|
|
|
|
|
|
def test_page_draft_blocks_direct_template_application(client):
|
|
sid=create(client);eid=home(client,sid)['id'];doc=v8_templates.export_template(sid,'Template')
|
|
assert client.put(f'/api/sites/{sid}/pages/{eid}/studio-draft',json={
|
|
'blocks':[layout()],'page_template':'home','expected_revision':0}).status_code==200
|
|
with pytest.raises(HTTPException) as exc:
|
|
v8_templates.apply_template(sid,doc,overwrite_pages=True)
|
|
assert exc.value.status_code==409
|
|
|
|
|
|
def test_midway_atomic_template_failure_cannot_partially_update(client):
|
|
sid=create(client)
|
|
doc=v8_templates.export_template(sid,'Template')
|
|
doc['pages'].append({'route':'/trigger/','page_type':'standard','blocks':[]})
|
|
assert len(doc['pages'])==2
|
|
original=db.one('SELECT theme,settings_json,accent FROM sites WHERE id=?',(sid,))
|
|
entry=db.one('SELECT meta_json FROM entries WHERE site_id=? AND route=?',(sid,'/'))
|
|
with db.connect() as c:
|
|
c.execute("""CREATE TRIGGER if_not_now BEFORE INSERT ON entries
|
|
WHEN NEW.route='/trigger/' BEGIN SELECT RAISE(ABORT,'test failure'); END""")
|
|
with pytest.raises(sqlite3.IntegrityError):
|
|
v8_templates.apply_template(sid,doc,overwrite_pages=True)
|
|
assert db.one('SELECT theme,settings_json,accent FROM sites WHERE id=?',(sid,))==original
|
|
assert db.one('SELECT meta_json FROM entries WHERE site_id=? AND route=?',(sid,'/'))==entry
|
|
assert not db.one('SELECT id FROM entries WHERE site_id=? AND route=?',(sid,'/trigger/'))
|
|
|
|
|
|
def test_editor_frontend_has_two_step_template_and_device_controls(client):
|
|
sid=create(client);eid=home(client,sid)['id']
|
|
assert 'Owner 发布模板' in client.get('/production-v8').text
|
|
assert 'setDeviceColumns' in client.get('/ui/editor.js').text
|
|
assert 'setColumnDevice' in client.get('/ui/editor.js').text
|
|
|
|
|
|
def test_nested_page_uses_root_relative_media_and_cta(client):
|
|
from pathlib import Path
|
|
sid=create(client)
|
|
folder=db.DATA/'media'/sid;folder.mkdir(parents=True,exist_ok=True)
|
|
image='0123456789ab.png';(folder/image).write_bytes(b'\x89PNG\r\n\x1a\n'+b'x'*10)
|
|
p=save_entry(sid,{'type':'page','title':'Nested detail','route':'/products/demo/',
|
|
'meta':{'layout_mode':'replace','blocks':[
|
|
blueprints.new_block('image_text',{'title':'Detail','image':'media/'+image,'href':'/contact/'}),
|
|
blueprints.new_block('cta',{'title':'Contact','href':'/contact/'})]}})
|
|
result=builder.build(sid,False)
|
|
out=(db.DATA/'builds'/sid/result['version']/'dist/products/demo/index.html').read_text()
|
|
assert 'src="/media/'+image+'"' in out
|
|
assert 'href="/contact/"' in out
|
|
assert 'href="contact/"' not in out
|
|
assert client.get(f'/preview/{sid}/{result["version"]}/products/demo/').status_code==200
|
|
assert f'/preview/{sid}/{result["version"]}/media/{image}' in client.get(f'/preview/{sid}/{result["version"]}/products/demo/').text
|
|
|
|
|
|
def test_editor_asset_picker_and_local_image_preview(client):
|
|
sid=create(client)
|
|
eid=home(client,sid)['id']
|
|
assert 'siteMediaModal' in client.get(f'/editor/{sid}/{eid}').text
|
|
assert 'chooseMedia' in client.get('/ui/editor.js').text
|
|
folder=db.DATA/'media'/sid;folder.mkdir(parents=True,exist_ok=True)
|
|
image='0123456789ab.png';(folder/image).write_bytes(b'\x89PNG\r\n\x1a\n'+b'x'*10)
|
|
preview=client.post(f'/api/sites/{sid}/pages/{eid}/editor-preview',json={
|
|
'blocks':[blueprints.new_block('image_text',{'image':'media/'+image})],
|
|
'page_template':'home','layout_mode':'replace'})
|
|
assert preview.status_code==200,preview.text[:800]
|
|
assert f'src="/local-media/{sid}/{image}"' in preview.text
|
|
assert client.get(f'/local-media/{sid}/{image}').status_code==200
|