chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec
- 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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user