"""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