upgrade: adopt V8.31.1 (theme 1.3/1.4 redesigns, editor deep-canvas, guided launch, component library)

Overlay 190 upstream items (V8.22→8.31) preserving trade-pro theme,
registry tests, and design docs. Re-applied full patch set on identical
anchors (discovery/env_for/blocks.css/type-mapping/locale-override/
theme_color/frontend entries/test_theme_pro_v3 mods) plus compose pins.

Verified: theme suites 35/35; dual-baseline full run 415 passed vs
pristine 386+29 ours, FAILED-set diff EMPTY (same 10 Windows env fails).
Live: container 8.31.1-dev.1 healthy, hesupipe 192 entries intact,
trade-pro build 203 pages 0 errors.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-26 09:43:17 +08:00
parent a63548c645
commit 8884e100f9
647 changed files with 20853 additions and 271 deletions
+1 -1
View File
@@ -133,7 +133,7 @@ def test_theme_gallery_and_compatibility(factory):
ver=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version']
html=factory.get(f'/preview/{sid}/{ver}/product/unique/').text
assert 'pro-gallery-thumbs' in html and 'data-gallery-image' in html
assert 'pro.js' in html
assert 'auto-compiled.js' in html or 'pro.js' in html
def test_css_style_breakout_rejected(factory):
+106
View File
@@ -0,0 +1,106 @@
"""V8.24: persisted 2-level layout editing, safe public rendering and migration compatibility."""
from pathlib import Path
import pytest
from bs4 import BeautifulSoup
from app import blueprints
from tests.test_factory import client,create
ROOT=Path(__file__).resolve().parents[1]
def make_nested():
root=blueprints.new_block('layout')
inner=blueprints.new_block('layout',{'gap':18})
leaf=blueprints.new_block('image_text',{'title':'Inner leaf title','description':'Nested content'})
inner['columns'][0]['blocks']=[leaf]
root['columns'][1]['blocks']=[inner]
return root,inner,leaf
def test_deep_nested_output_valid_and_safe():
root,inner,leaf=make_nested()
clean=blueprints.validate_blocks([root])
markup=blueprints.render_blocks(clean,{'name':'Example'},{'title':'Home'},[])
soup=BeautifulSoup(markup,'html.parser')
assert soup.select_one(f'.sf-layout-grid[data-layout="{root["id"]}"]')
assert soup.select_one(f'.sf-layout-grid[data-layout="{inner["id"]}"]')
assert soup.select_one(f'.sf-layout-column .sf-section[data-block="{leaf["id"]}"]')
assert 'Inner leaf title' in soup.get_text(' ')
assert 'siteforge-studio-canvas' not in markup # never emitted into public HTML
def test_reject_exceeding_allowed_nested_layout_depth():
root,inner,_=make_nested()
extra=blueprints.new_block('layout')
inner['columns'][0]['blocks'].append(extra)
with pytest.raises(ValueError,match='最多嵌套两层'):
blueprints.validate_blocks([root])
def test_reject_duplicate_ids_in_deep_nested_layout():
root,inner,leaf=make_nested()
inner['columns'][1]['blocks'].append({**leaf})
with pytest.raises(ValueError,match='重复'):
blueprints.validate_blocks([root])
def test_deep_nested_draft_and_history_round_trip(client):
sid=create(client)
eid=next(row['id'] for row in client.get(f'/api/sites/{sid}/entries').json() if row['route']=='/')
root,inner,leaf=make_nested()
url=f'/api/sites/{sid}/pages/{eid}/studio-draft'
saved=client.put(url,json={'blocks':[root],'page_template':'home','layout_mode':'replace','expected_revision':0})
assert saved.status_code==200,saved.text
loaded=client.get(url);assert loaded.status_code==200
assert loaded.json()['blocks'][0]['columns'][1]['blocks'][0]['columns'][0]['blocks'][0]['id']==leaf['id']
assert client.put(url,json={'blocks':[root],'page_template':'home','layout_mode':'replace','expected_revision':0}).status_code==409
def test_deep_nested_editor_helpers_and_isolated_script():
helper=(ROOT/'app/static/editor-helpers.js').read_text()
editor=(ROOT/'app/static/editor.js').read_text()
bridge=(ROOT/'app/static/studio-canvas-bridge.js').read_text()
assert 'findChildInRoot' in helper
assert 'findLayout' in helper
assert 'setNestedLayoutPreset' in editor
assert 'setNestedLayoutGap' in editor
assert 'childQuickEditor' in editor
assert 'siteforge-studio-canvas' in bridge
assert 'event.source!==frame.contentWindow' in editor
assert 'eval(' not in bridge and 'new Function(' not in bridge
def test_nested_preview_endpoint_and_responsive_values(client):
sid=create(client)
eid=next(row['id'] for row in client.get(f'/api/sites/{sid}/entries').json() if row['route']=='/')
root,inner,leaf=make_nested()
inner['mobile_columns']=1
inner['tablet_columns']=2
inner['gap']=32
preview=client.post(f'/api/sites/{sid}/pages/{eid}/editor-preview',json={
'blocks':[root],'page_template':'home','layout_mode':'replace'})
assert preview.status_code==200,preview.text[:600]
assert 'Inner leaf title' in preview.text
assert 'data-layout="'+inner['id']+'"' in preview.text
assert 'studio-canvas-bridge.js' in preview.text or 'siteforge-studio-canvas' in preview.text
def test_editor_uses_no_json_input_for_deep_nested_components():
source=(ROOT/'app/static/editor.js').read_text()
for feature in ('addNestedChild','setNestedDeviceColumns','setNestedLayoutGap','setNestedLayoutPreset',
'findChildInRoot','findLayout','Studio.applyMedia(r.url)'):
assert feature in source
def test_save_nested_leaf_as_site_scoped_reusable_component(client):
sid=create(client)
_,_,leaf=make_nested()
created=client.post(f'/api/sites/{sid}/components',json={'name':'海运图文', 'block':leaf})
assert created.status_code==200,created.text
cid=created.json()['id']
loaded=client.get(f'/api/sites/{sid}/components')
assert loaded.status_code==200
assert any(row['id']==cid and row['block']['title']=='Inner leaf title' for row in loaded.json())
stale=client.put(f'/api/sites/{sid}/components/{cid}',json={
'name':'不允许覆盖','expected_version':0,'block':leaf})
assert stale.status_code==409
+4 -3
View File
@@ -18,8 +18,9 @@ def test_editor_add_and_direct_canvas_selection():
api=(ROOT/'app/main.py').read_text()
for method in ('toggleLibrary(force)','updateGuidance()','showHelp()','select(i)','arrayEditor(field,value,type)','itemField(field,index,key,value)','addItem(field)','removeItem(field,index)','moveItem(field,index,by)'):
assert method in js
assert "siteforge-select-block" in js
assert "siteforge-select-block" in api
assert "siteforge-studio-canvas" in js
assert "studio-canvas-bridge.js" in api
assert "safeCanvasCommand" in js
assert 'event.source!==frame.contentWindow' in js
assert "studio-draft/publish" in js
assert 'expected_revision' in js
@@ -30,4 +31,4 @@ def test_no_change_to_block_blueprint_or_database_schema():
assert demo.exists()
assert '演示操作不会修改正式网站' in demo.read_text()
from app.version import VERSION
assert VERSION.startswith('8.21.') and VERSION.endswith('-dev.1')
assert VERSION.startswith('8.') and VERSION.endswith('-dev.1')
+3 -2
View File
@@ -66,7 +66,8 @@ def test_complete_bootstrap_repeated_safe(factory):
assert all(r['route'] for r in records)
build=factory.post(f'/api/sites/{sid}/builds',json={}).json()
html=factory.get(f'/preview/{sid}/{build["version"]}/').text
assert 'sf-site-head' in html and 'factory.css' in html
assert 'sf-site-head' in html
assert 'auto-compiled.css' in html or 'factory.css' in html
assert factory.get(f'/preview/{sid}/{build["version"]}/assets/site-runtime.js').status_code==200
def test_mapping_preview_commit_and_cross_site(factory):
@@ -104,7 +105,7 @@ def test_editor_supports_responsive_settings(factory):
assert r.status_code==200,r.text
preview=factory.post(f'/api/sites/{sid}/pages/{homepage["id"]}/editor-preview',json={'blocks':blocks,'page_template':'home'})
assert preview.status_code==200
assert 'siteforge-inline-editor' in preview.text and '--sf-pad:19px' in preview.text
assert 'siteforge-studio-canvas' in preview.text and '--sf-pad:19px' in preview.text
build=factory.post(f'/api/sites/{sid}/builds',json={}).json()
css=factory.get(f'/preview/{sid}/{build["version"]}/assets/custom.css').text
assert '--sf-pad:19px' in css and 'display:none' in css
+22
View File
@@ -0,0 +1,22 @@
const assert=require('node:assert/strict');
const H=require('../app/static/editor-helpers.js');
const layout={id:'layout_0001',type:'layout',columns:[{width:1,blocks:[{id:'child_0001',type:'cta',title:'A'},{id:'child_0002',type:'cta',title:'B'}]},{width:1,blocks:[]}]};
const rows=[layout];const spec={cta:{fields:['title','subtitle','href']}};
assert.equal(H.findNestedChild(rows,'layout_0001','child_0002').child.title,'B');
assert.equal(H.findNestedChild(rows,'layout_0001','unknown'),null);
assert.equal(H.findNestedChild(rows,'unknown','child_0001'),null);
assert.equal(H.safeNestedCanvasCommand({action:'child-select',blockId:'layout_0001',childId:'child_0001'},rows,spec),true);
assert.equal(H.safeNestedCanvasCommand({action:'child-inline-text',blockId:'layout_0001',childId:'child_0001',field:'title',value:'New'},rows,spec),true);
assert.equal(H.safeNestedCanvasCommand({action:'child-inline-text',blockId:'layout_0001',childId:'child_0001',field:'body',value:'<script>'},rows,spec),false);
assert.equal(H.safeNestedCanvasCommand({action:'child-delete',blockId:'other',childId:'child_0001'},rows,spec),false);
assert.equal(H.safeNestedCanvasCommand({action:'child-execute',blockId:'layout_0001',childId:'child_0001'},rows,spec),false);
assert.equal(H.safeNestedCanvasCommand({action:'child-inline-text',blockId:'layout_0001',childId:'child_0001',field:'title',value:'x'.repeat(501)},rows,spec),false);
assert.deepEqual(H.layoutPreset(layout,'equal'),[1,1]);
assert.deepEqual(H.layoutPreset(layout,'left-wide'),[2,1]);
assert.deepEqual(H.layoutPreset(layout,'right-wide'),[1,2]);
assert.equal(H.layoutPreset(layout,'script'),null);
assert.deepEqual(H.moveNested(layout,0,1,-1).map(x=>x.id),['child_0002','child_0001']);
assert.equal(H.moveNested(layout,0,0,-1),null);
const cp=H.duplicate(layout.columns[0].blocks[0],()=> 'child_0003');assert.equal(cp.id,'child_0003');
assert.equal(layout.columns[0].blocks[0].id,'child_0001');
console.log('PASS: V8.23 nested helper checks');
+57
View File
@@ -0,0 +1,57 @@
"""V8.23 nested authoring contract. These checks execute against the real renderer."""
from pathlib import Path
from bs4 import BeautifulSoup
from app import blueprints
from tests.test_factory import client,create
ROOT=Path(__file__).resolve().parents[1]
def test_nested_ids_are_rendered_and_duplicate_ids_rejected():
layout=blueprints.new_block('layout')
child=blueprints.new_block('image_text',{'title':'Subcomponent editing'})
layout['columns'][0]['blocks']=[child]
clean=blueprints.validate_blocks([layout]);assert clean[0]['columns'][0]['blocks'][0]['id']==child['id']
html=blueprints.render_blocks(clean,{}, {},[])
soup=BeautifulSoup(html,'html.parser')
assert soup.select_one(f'.sf-layout-grid[data-layout="{layout["id"]}"]')
assert soup.select_one(f'.sf-layout-column .sf-section[data-block="{child["id"]}"]')
assert 'onload=' not in html
layout['columns'][1]['blocks']=[child]
import pytest
with pytest.raises(ValueError,match='重复'):blueprints.validate_blocks([layout])
def test_nested_view_is_only_in_draft_editor():
editor=(ROOT/'app/static/editor.js').read_text(encoding='utf-8')
bridge=(ROOT/'app/static/studio-canvas-bridge.js').read_text(encoding='utf-8')
helpers=(ROOT/'app/static/editor-helpers.js').read_text(encoding='utf-8')
for marker in ('child-select','child-inline-text','child-duplicate','child-delete'):
assert marker in bridge and marker in editor
assert 'safeNestedCanvasCommand' in helpers
assert 'childQuickEditor' in editor and 'setLayoutPreset' in editor
assert 'event.source!==frame.contentWindow' in editor
assert 'eval(' not in bridge and 'new Function(' not in bridge
assert 'studio-canvas-bridge.js' not in (ROOT/'app/builder.py').read_text(encoding='utf-8')
def test_editor_sandbox_keeps_scripts_isolated():
soup=BeautifulSoup((ROOT/'app/templates/editor.html').read_text(encoding='utf-8'),'html.parser')
assert soup.select_one('#sitePreview')['sandbox']==['allow-scripts']
assert soup.select_one('#saveDraftButton')
def test_nested_layout_draft_round_trip(client):
sid=create(client)
entries=client.get(f'/api/sites/{sid}/entries').json()
eid=next(row['id'] for row in entries if row['route']=='/')
parent=blueprints.new_block('layout')
child=blueprints.new_block('image_text',{'title':'Nested draft stable'})
parent['columns'][0]['blocks']=[child]
uri=f'/api/sites/{sid}/pages/{eid}/studio-draft'
payload={'blocks':[parent],'page_template':'home','layout_mode':'replace','expected_revision':0}
put=client.put(uri,json=payload)
assert put.status_code==200,put.text
result=client.get(uri).json()
assert result['blocks'][0]['columns'][0]['blocks'][0]['title']=='Nested draft stable'
assert result['revision']==1
assert client.put(uri,json=payload).status_code==409
+7 -3
View File
@@ -19,7 +19,10 @@ def test_four_complete_theme_families_have_distinct_markup(factory):
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 theme == 'automotive':
assert 'preview.bundle.css' in r.text and 'preview.bundle.js' in r.text
else:
assert 'pro-base.css' in r.text and 'pro.js' in r.text
if page_type=='product' and theme!='trade-pro':assert soup.select_one('.pro-product-layout .pro-specs') # trade-pro covered by test_theme_registry
if page_type=='listing':assert soup.select_one('[data-pro-filter-search]')
if page_type=='article':assert soup.select_one('.pro-article-hero')
@@ -65,8 +68,9 @@ def test_theme_build_derived_category_brand_search_and_seo(factory):
def test_builtin_version_pin_is_real(factory):
sid=site(factory)
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code==200
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 {v['version'] for v in versions}=={'1.4.1','1.4.0','1.3.0','1.2.0','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
@@ -74,7 +78,7 @@ def test_builtin_version_pin_is_real(factory):
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
assert 'pro-auto-hero' in new and 'auto31-home' not in new
def test_custom_theme_package_uses_trusted_layout_and_css(factory):
+22
View File
@@ -0,0 +1,22 @@
"""V8.22 studio canvas wiring and editor-published separation."""
from pathlib import Path
from bs4 import BeautifulSoup
ROOT=Path(__file__).resolve().parents[1]
def test_canvas_resources_are_real_and_editor_only():
bridge=(ROOT/'app/static/studio-canvas-bridge.js').read_text(encoding='utf-8')
source=(ROOT/'app/main.py').read_text(encoding='utf-8')
editor=(ROOT/'app/static/editor.js').read_text(encoding='utf-8')
assert 'studio-canvas-bridge.js' in source
assert 'studio-canvas-bridge.js' not in (ROOT/'app/builder.py').read_text(encoding='utf-8')
for command in ('reorder','resize-columns','add-after','duplicate','delete','inline-text'):
assert command in editor and command in bridge
assert 'eval(' not in bridge and 'new Function(' not in bridge
assert 'event.source!==frame.contentWindow' in editor
def test_editor_canvas_hints_and_save_remain_accessible():
soup=BeautifulSoup((ROOT/'app/templates/editor.html').read_text(encoding='utf-8'),'html.parser')
assert soup.select_one('#sfInsertHint')['hidden'] == ''
assert soup.select_one('#sitePreview')['sandbox']==['allow-scripts']
assert soup.select_one('#saveDraftButton')
assert soup.select_one('#publishDesignButton')
+209
View File
@@ -0,0 +1,209 @@
"""V8.25 opt-in shared component correctness, isolation and draft safety."""
import copy
import json
import pytest
from tests.test_factory import client,create
from app import db,blueprints
def home(c,site):
return next(row for row in c.get(f'/api/sites/{site}/entries').json() if row['route']=='/')
def library(c,site,title='SHARED_OLD'):
block=blueprints.new_block('cta',{'title':title})
r=c.post(f'/api/sites/{site}/components',json={'name':'公共行动区','block':block})
assert r.status_code==200,r.text
return r.json()
def link(c,site,entry,item):
block=copy.deepcopy(item['block'])
block['id']='link_'+entry['id'][:10]
block['shared_ref']={'id':item['id'],'version':item['version'],'hash':item['snapshot_hash']}
url=f'/api/sites/{site}/pages/{entry["id"]}/studio-draft'
draft=c.get(url).json()
r=c.put(url,json={'blocks':[block],'page_template':'standard','layout_mode':'replace','expected_revision':draft['revision']})
assert r.status_code==200,r.text
return url,block
def change(c,site,item,title='SHARED_NEW'):
updated=blueprints.new_block('cta',{'title':title})
r=c.put(f'/api/sites/{site}/components/{item["id"]}',json={
'name':'公共行动区','expected_version':item['version'],'block':updated})
assert r.status_code==200,r.text
return r.json()
def test_linked_usage_and_explicit_sync_never_modifies_publication(client):
site=create(client);entry=home(client,site);original=db.one('SELECT meta_json FROM entries WHERE id=?',(entry['id'],))['meta_json']
item=library(client,site);url,block=link(client,site,entry,item)
v2=change(client,site,item)
base=f'/api/sites/{site}/components/{item["id"]}'
preview=client.get(base+'/usage').json()
assert preview['references'][0]['eligible']
assert preview['references'][0]['block_id']==block['id']
assert preview['references'][0]['current_version']==1
r=client.post(base+'/sync',json={'plan_token':preview['plan_token'],'entry_ids':[entry['id']]})
assert r.status_code==200,r.text
assert r.json()['published_unchanged']
newer=client.get(url).json()
assert newer['revision']==2
assert newer['blocks'][0]['title']=='SHARED_NEW'
assert newer['blocks'][0]['id']==block['id']
assert newer['blocks'][0]['shared_ref']['version']==2
assert db.one('SELECT meta_json FROM entries WHERE id=?',(entry['id'],))['meta_json']==original
assert client.get(base+'/usage').json()['references'][0]['up_to_date']
history=client.get(f'/api/sites/{site}/pages/{entry["id"]}/studio-history').json()['items']
assert len(history)==2
v1=client.get(base+'/versions/1').json();v2view=client.get(base+'/versions/2').json()
assert v1['block']['title']=='SHARED_OLD' and v2view['block']['title']=='SHARED_NEW'
assert v1['snapshot_hash']!=v2['snapshot_hash']
def test_edited_instances_are_not_overwritten(client):
site=create(client);entry=home(client,site);item=library(client,site)
url,block=link(client,site,entry,item)
current=client.get(url).json()
current['blocks'][0]['title']='LOCALLY_CUSTOMIZED'
payload={'blocks':current['blocks'],'page_template':'standard','layout_mode':'replace','expected_revision':1}
assert client.put(url,json=payload).status_code==200
change(client,site,item)
root=f'/api/sites/{site}/components/{item["id"]}'
preview=client.get(root+'/usage').json()
assert preview['references'][0]['local_modified']
assert not preview['references'][0]['eligible']
response=client.post(root+'/sync',json={'plan_token':preview['plan_token'],'entry_ids':[entry['id']]})
assert response.status_code==409
assert client.get(url).json()['blocks'][0]['title']=='LOCALLY_CUSTOMIZED'
def test_stale_preview_and_foreign_site_protected(client):
a=create(client);b=create(client);entry=home(client,a)
item=library(client,a);url,_=link(client,a,entry,item)
change(client,a,item)
src=f'/api/sites/{a}/components/{item["id"]}'
preview=client.get(src+'/usage').json()
assert client.get(f'/api/sites/{b}/components/{item["id"]}/usage').status_code==404
assert client.get(f'/api/sites/{b}/components/{item["id"]}/versions').status_code==404
assert client.post(f'/api/sites/{b}/components/{item["id"]}/sync',json={
'plan_token':preview['plan_token'],'entry_ids':[entry['id']]}).status_code==404
draft=client.get(url).json()
assert client.put(url,json={**{k:draft[k] for k in ('blocks','page_template','layout_mode')},
'expected_revision':draft['revision']}).status_code==200
assert client.post(src+'/sync',json={'plan_token':preview['plan_token'],'entry_ids':[entry['id']]}).status_code==409
def test_unlinked_copies_not_counted_and_deletion_blocked(client):
site=create(client);item=library(client,site);entry=home(client,site)
block=copy.deepcopy(item['block'])
r=client.put(f'/api/sites/{site}/pages/{entry["id"]}/studio-draft',json={
'blocks':[block],'page_template':'standard','expected_revision':0})
assert r.status_code==200
root=f'/api/sites/{site}/components/{item["id"]}'
assert client.get(root+'/usage').json()['references']==[]
link(client,site,entry,item)
assert client.delete(root).status_code==409
other=library(client,site,'OTHER')
assert client.delete(f'/api/sites/{site}/components/{other["id"]}').status_code==200
def test_invalid_component_ref_rejected(client):
site=create(client);entry=home(client,site)
item=blueprints.new_block('cta')
item['shared_ref']={'id':'<script>','version':1,'hash':'0'*64}
r=client.put(f'/api/sites/{site}/pages/{entry["id"]}/studio-draft',json={
'blocks':[item],'page_template':'standard','expected_revision':0})
assert r.status_code==400
def test_multiple_page_sync_rolls_back_on_conflict(client):
site=create(client);item=library(client,site)
one=home(client,site)
second=client.post(f'/api/sites/{site}/entries',json={'type':'page','title':'Other','route':'/second/'}).json()
assert second['id']
first_url,_=link(client,site,one,item);second_url,_=link(client,site,second,item)
change(client,site,item)
base=f'/api/sites/{site}/components/{item["id"]}'
usage=client.get(base+'/usage').json()
assert len(usage['references'])==2
err=client.post(base+'/sync',json={'entry_ids':[one['id'],second['id']],'plan_token':'f'*64})
assert err.status_code==409
assert client.get(first_url).json()['revision']==1
result=client.post(base+'/sync',json={'entry_ids':[one['id'],second['id']],
'plan_token':usage['plan_token']})
assert result.status_code==200,result.text
assert len(result.json()['updated'])==2
assert client.get(first_url).json()['revision']==2
assert client.get(second_url).json()['revision']==2
def test_two_page_sync_is_a_single_transaction(client,monkeypatch):
from fastapi import HTTPException
from app import reusables
site=create(client);item=library(client,site)
a=home(client,site)
b=client.post(f'/api/sites/{site}/entries',json={'type':'page','title':'Second','route':'/multi/'}).json()
a_url,_=link(client,site,a,item)
b_url,_=link(client,site,b,item)
change(client,site,item)
base=f'/api/sites/{site}/components/{item["id"]}'
usage=client.get(base+'/usage').json()
original=reusables._copy_into
called=[0]
def break_on_second(*args,**kwargs):
called[0]+=1
if called[0]==2:raise HTTPException(409,'模拟第二页失败')
return original(*args,**kwargs)
monkeypatch.setattr(reusables,'_copy_into',break_on_second)
response=client.post(base+'/sync',json={
'entry_ids':[a['id'],b['id']],'plan_token':usage['plan_token']})
assert response.status_code==409
assert called[0]==2
assert client.get(a_url).json()['revision']==1
assert client.get(b_url).json()['revision']==1
def test_published_link_is_not_republished_by_library_sync(client):
site=create(client);page=home(client,site);item=library(client,site)
url,_=link(client,site,page,item)
assert client.post(url+'/publish',json={'expected_revision':1}).status_code==200
before=db.one('SELECT meta_json FROM entries WHERE id=?',(page['id'],))['meta_json']
assert 'shared_ref' in before
change(client,site,item)
base=f'/api/sites/{site}/components/{item["id"]}'
usage=client.get(base+'/usage').json()
assert usage['references'][0]['eligible'] and not usage['references'][0]['has_draft']
response=client.post(base+'/sync',json={'plan_token':usage['plan_token'],'entry_ids':[page['id']]})
assert response.status_code==200,response.text
assert db.one('SELECT meta_json FROM entries WHERE id=?',(page['id'],))['meta_json']==before
assert client.get(url).json()['state']=='draft'
def test_repeated_migration_and_recursive_detach(client):
from app import reusables
site=create(client)
other=library(client,site)
layout=blueprints.new_block('layout')
nested=blueprints.new_block('cta')
nested['shared_ref']={'id':other['id'],'version':1,'hash':other['snapshot_hash']}
layout['columns'][0]['blocks']=[nested]
saved=client.post(f'/api/sites/{site}/components',json={'name':'新布局','block':layout})
assert saved.status_code==200,saved.text
assert 'shared_ref' not in saved.json()['block']['columns'][0]['blocks'][0]
reusables.setup();reusables.setup()
history=client.get(f'/api/sites/{site}/components/{saved.json()["id"]}/versions').json()
assert len(history['items'])==1
def test_shared_editor_has_clear_actions():
from pathlib import Path
root=Path(__file__).resolve().parents[1]
html=(root/'app/templates/editor.html').read_text()
js=(root/'app/static/editor.js').read_text()
assert 'sharedComponentModal' in html
for method in ('sharedUsage','sharedApply','sharedVersions','detachShared','insertReusable'):
assert method in js
assert '复制:独立编辑' in html
assert '关联插入' in js
+104
View File
@@ -0,0 +1,104 @@
"""V8.26 global chrome review, version snapshots, and safe restoration."""
import copy
import json
from tests.test_factory import client, create
from app import db, site_design_v87
def config(marker='Global HESU Header'):
data=copy.deepcopy(site_design_v87.DEFAULTS)
data['header'].update({'announcement': marker, 'show_announcement': True})
data['footer']['title']='HESU Footer'
data['nav']=[{'title':'Products','route':'/products/','children':[
{'title':'Vehicles','route':'/vehicles/','children':[]}]}]
return data
def test_human_review_and_reviewed_publication_are_build_isolated(client):
site=create(client);url=f'/api/sites/{site}/site-design'
old=db.one('SELECT settings_json FROM sites WHERE id=?',(site,))['settings_json']
assert client.put(url,json={'config':config(), 'expected_revision':0}).status_code==200
proof=client.get(url+'/review');assert proof.status_code==200,proof.text
body=proof.json()
assert {'header','nav','footer'}.issubset(set(body['changed_sections']))
assert body['published_site_unchanged'] is True
assert body['change_count']>=3
assert db.one('SELECT settings_json FROM sites WHERE id=?',(site,))['settings_json']==old
wrong=client.post(url+'/publish-reviewed',json={'expected_revision':1,'plan_token':'1'*64})
assert wrong.status_code==409
good=client.post(url+'/publish-reviewed',json={'expected_revision':1,'plan_token':body['plan_token']})
assert good.status_code==200,good.text
assert 'Global HESU Header' in db.one('SELECT settings_json FROM sites WHERE id=?',(site,))['settings_json']
assert client.get(url+'/review').status_code==409
def test_old_review_token_invalid_after_draft_edit(client):
site=create(client);url=f'/api/sites/{site}/site-design'
assert client.put(url,json={'config':config('First'), 'expected_revision':0}).status_code==200
stale=client.get(url+'/review').json()
assert client.put(url,json={'config':config('Second'), 'expected_revision':1}).status_code==200
assert client.post(url+'/publish-reviewed',json={
'expected_revision':2,'plan_token':stale['plan_token']}).status_code==409
fresh=client.get(url+'/review').json()
assert fresh['plan_token']!=stale['plan_token']
assert client.post(url+'/publish-reviewed',json={
'expected_revision':2,'plan_token':fresh['plan_token']}).status_code==200
def test_restore_history_as_new_draft_does_not_mutate_live(client):
site=create(client);url=f'/api/sites/{site}/site-design'
assert client.put(url,json={'config':config('Release A'),'expected_revision':0}).status_code==200
reviewed=client.get(url+'/review').json()
pub=client.post(url+'/publish-reviewed',json={'expected_revision':1,'plan_token':reviewed['plan_token']}).json()
pid=pub['publication_id']
old_live=db.one('SELECT settings_json FROM sites WHERE id=?',(site,))['settings_json']
history=client.get(url+f'/publications/{pid}');assert history.status_code==200
assert history.json()['after']['header']['announcement']=='Release A'
assert any(x['field']=='header.announcement' for x in history.json()['changes'])
live_hash=client.get(url+'/live-hash').json()['live_hash']
recovered=client.post(url+f'/publications/{pid}/restore',json={
'snapshot':'before','expected_revision':0,'expected_live_hash':live_hash})
assert recovered.status_code==200,recovered.text
assert recovered.json()['published_unchanged']
assert client.get(url).json()['revision']==1
assert db.one('SELECT settings_json FROM sites WHERE id=?',(site,))['settings_json']==old_live
assert client.post(url+f'/publications/{pid}/restore',json={
'snapshot':'before','expected_revision':0,'expected_live_hash':live_hash}).status_code==409
def test_restore_rejects_stale_live_site_and_foreign_history(client):
a=create(client);b=create(client)
ua=f'/api/sites/{a}/site-design';ub=f'/api/sites/{b}/site-design'
assert client.put(ua,json={'config':config(), 'expected_revision':0}).status_code==200
preview=client.get(ua+'/review').json()
pid=client.post(ua+'/publish-reviewed',json={
'expected_revision':1,'plan_token':preview['plan_token']}).json()['publication_id']
old_hash=client.get(ua+'/live-hash').json()['live_hash']
assert client.get(ub+f'/publications/{pid}').status_code==404
assert client.post(ub+f'/publications/{pid}/restore',json={
'snapshot':'after','expected_revision':0,'expected_live_hash':old_hash}).status_code==404
current=db.one('SELECT settings_json FROM sites WHERE id=?',(a,))
changed=json.loads(current['settings_json']);changed['external_setting']='changed'
db.execute('UPDATE sites SET settings_json=? WHERE id=?',(json.dumps(changed),a))
assert client.post(ua+f'/publications/{pid}/restore',json={
'snapshot':'after','expected_revision':0,'expected_live_hash':old_hash}).status_code==409
assert client.post(ua+f'/publications/{pid}/restore',json={
'snapshot':'bad','expected_revision':0,'expected_live_hash':'a'*64}).status_code==400
def test_nav_editor_is_visual_and_review_ui_is_wired(client):
sid=create(client)
entry=next(x for x in client.get(f'/api/sites/{sid}/entries').json() if x['route']=='/')
page=client.get(f'/editor/{sid}/{entry["id"]}').text
assert 'siteNavRows' in page and '+ 添加一级菜单' in page
assert 'siteDesignReviewModal' in page and 'siteDesignHistoryModal' in page
code=client.get('/ui/editor.js').text
assert 'drawSiteNav()' in code and 'publish-reviewed' in code
assert 'showSiteDesignHistory' in code
def test_invalid_navigation_remains_rejected(client):
sid=create(client);url=f'/api/sites/{sid}/site-design'
bad=config();bad['nav'][0]['children'][0]['route']='javascript:alert(1)'
assert client.put(url,json={'config':bad,'expected_revision':0}).status_code==400
assert client.get(url).json()['revision']==0
+33
View File
@@ -0,0 +1,33 @@
"""V8.27 novice-facing console: UI wiring + live API smoke."""
from pathlib import Path
from test_factory import client
ROOT=Path(__file__).resolve().parents[1]
def test_workspace_has_progress_and_safe_guidance(client):
r=client.get('/')
assert r.status_code==200
for text in ('建站操作指引','createProgressText','createSubmit','选择风格','确认创建','siteModal'):
assert text in r.text
assert '/ui/workspace-guide.js' in r.text
assert r.text.index('/ui/workspace-guide.js')<r.text.index('/ui/workspace.js')
assert client.get('/ui/workspace-guide.js').status_code==200
def test_guide_is_real_navigation_and_real_api(client):
js=client.get('/ui/workspace.js').text
for phrase in ('function guide()', 'function renderCreateStep()', 'function openCreateWizard()', "await api('/api/sites','POST',data)", "await selectSite(s.id,'guide')"):
assert phrase in js
assert '正式上线前需要发布条件检查' in client.get('/ui/workspace-guide.js').text
create=client.post('/api/sites',json={'name':'引导式新站','theme':'corporate','mode':'new'})
assert create.status_code==201
data=create.json()
entries=client.get(f"/api/sites/{data['id']}/entries").json()
assert len(entries)>=1
assert any(x['source']=='starter' for x in entries)
def test_existing_release_controls_and_permission_barriers_preserved(client):
assert client.get('/api/console/bootstrap').status_code==200
ws=client.get('/ui/workspace.js').text
for phrase in ('releasePreflight()', 'releaseApproval()', 'releaseDeploy()', 'releaseRollback('):
assert phrase in ws
assert client.get('/classic').status_code==200
+37
View File
@@ -0,0 +1,37 @@
"""V8.28: secured user journey remains mapped to existing FastAPI controls."""
from pathlib import Path
from test_factory import client
ROOT=Path(__file__).resolve().parents[1]
def test_launch_center_is_reachable_from_workspace(client):
response=client.get('/')
assert response.status_code==200
assert 'data-view="launch"' in response.text
assert '检查并上线' in response.text
assert '/ui/workspace-launch.js' in response.text
assert client.get('/ui/workspace-launch.js').status_code==200
def test_steps_use_existing_production_api_and_preserve_csrf(client):
script=client.get('/ui/workspace.js').text
for name in ('function launch()', 'function launchRequestApproval()', 'function launchPreflight()', 'function launchDeploy()', 'function resetLaunch()'):
assert name in script
for route in ('/release-requests', '/preflight/', '/deploy'):
assert route in script
assert "headers['X-SiteForge-CSRF']=state.csrf" in script
assert 'Idempotency-Key' in script
assert '已经检查此版本' in script
assert '提交部署只是创建任务' in script
def test_preflight_for_nonexistent_build_is_not_marked_ready(client):
# The administrative fixture is a superadmin. Missing builds must still fail closed.
result=client.get('/api/production/sites/not-your-website/preflight/something')
assert result.status_code in (403,404) or (result.status_code==200 and result.json().get('ready') is False)
def test_new_client_side_gates_do_not_change_server_approval_requirement(client):
site=client.post('/api/sites',json={'name':'V828 审批保护','theme':'corporate','mode':'new'})
assert site.status_code==201
sid=site.json()['id']
# Unauthorized to deploy a nonexistent or unapproved version, even if client is bypassed.
response=client.post(f'/api/production/sites/{sid}/deploy',json={'build_id':'fake-build','target':'production'})
assert response.status_code in (400,403,404,409,503)
+59
View File
@@ -0,0 +1,59 @@
"""V8.29 theme versioning and editor usability: protected integration checks."""
import json
from pathlib import Path
from bs4 import BeautifulSoup
from test_factory_v2 import factory, site
from app import theme_store
THEMES=('automotive','industrial','corporate','minimal')
ROOT=Path(__file__).resolve().parents[1]
def test_all_themes_preserve_v110_and_publish_v120(factory):
for theme in THEMES:
assert theme_store.latest(theme)==('1.4.1' if theme=='automotive' else '1.3.0')
assert theme_store.resolve(theme,'1.1.0').is_dir()
assert theme_store.compatibility(theme,'1.1.0')['valid']
assert theme_store.compatibility(theme,'1.2.0')['valid']
assert json.loads((theme_store.resolve(theme,'1.1.0')/'manifest.json').read_text())['version']=='1.1.0'
def test_theme_versions_remain_buildable(factory):
sid=site(factory)
for theme in THEMES:
for version in ('1.1.0','1.2.0','1.3.0'):
res=factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':theme,'version':version})
assert res.status_code==200, (theme,version,res.text)
res=factory.post(f'/api/sites/{sid}/builds',json={})
assert res.status_code==200,(theme,version,res.text)
bid=res.json()['version']
main=factory.get(f'/preview/{sid}/{bid}/')
assert main.status_code==200
assert f'pro-{theme}' in main.text
css=factory.get(f'/preview/{sid}/{bid}/assets/pro.css')
assert css.status_code==200
assert ('V8.29 presentation refinement' in css.text)==(version in ('1.2.0','1.3.0'))
def test_theme_preview_6_page_types(factory):
for theme in THEMES:
for kind in ('home','listing','product','article','about','contact'):
res=factory.get(f'/api/factory/themes/{theme}/preview',params={'page_type':kind})
assert res.status_code==200,(theme,kind)
assert BeautifulSoup(res.text,'html.parser').find('header',class_='pro-header')
assert 'V8.30 REAL PAGE COMPOSITION' in factory.get(f'/api/factory/themes/{theme}/preview.pro.css').text
def test_editor_controls_and_safe_preview_dialog(factory):
sid=site(factory)
# Real authenticated page route; selectors target actual shipped template.
res=factory.get(f'/studio/{sid}')
if res.status_code==404:
import re
editor=(ROOT/'app/templates/editor.html').read_text()
else:editor=res.text
for ident in ('studioThemePreviewButton','themePreviewModal','themePreviewTheme','themePreviewPage','themePreviewFrame','paletteSearch','quickUndo','quickRedo'):
assert f'id="{ident}"' in editor
assert 'sandbox="allow-scripts"' in editor
assert 'allow-same-origin' not in editor
assert 'studio-experience-v829.js' in editor
+60
View File
@@ -0,0 +1,60 @@
"""V8.30 shipped page-composition regression: data-safe, versioned, six real page types."""
from pathlib import Path
from bs4 import BeautifulSoup
from app import theme_store
from test_factory_v2 import factory,site
ROOT=Path(__file__).resolve().parents[1]
THEMES=('automotive','industrial','corporate','minimal')
KINDS=('home','listing','product','article','about','contact')
def test_new_and_legacy_page_layouts_are_version_isolated(factory):
for name in THEMES:
assert theme_store.latest(name)==('1.4.1' if name=='automotive' else '1.3.0')
assert theme_store.compatibility(name,'1.2.0')['valid']
assert theme_store.compatibility(name,'1.3.0')['valid']
old=theme_store.resolve(name,'1.2.0')
new=theme_store.resolve(name,'1.3.0')
assert 'pro-v30-page' not in (old/'pages/product.html').read_text()
assert 'pro-v30-page' in (new/'pages/product.html').read_text()
assert 'V8.30 REAL PAGE COMPOSITION' not in (old/'pro.css').read_text()
assert 'V8.30 REAL PAGE COMPOSITION' in (new/'pro.css').read_text()
def test_all_six_real_page_types_have_useful_structure(factory):
for name in THEMES:
for kind in KINDS:
response=factory.get(f'/api/factory/themes/{name}/preview',params={'page_type':kind})
assert response.status_code==200,(name,kind,response.text[:200])
doc=BeautifulSoup(response.text,'html.parser')
assert len(doc.select('h1'))==1,(name,kind)
assert doc.select_one('.pro-header') and doc.select_one('.pro-footer')
if kind=='home':assert doc.select_one('.pro-home-hero')
else:assert doc.select_one('.pro-v30-'+kind),(name,kind)
if kind=='listing':
assert doc.select_one('.pro-filter-scope')
assert doc.select_one('[data-pro-filter-search]')
if kind=='product':
assert doc.select_one('.pro-specs')
assert doc.select_one('.pro-product-media')
if kind=='article':assert doc.select_one('.pro-v30-reading-content')
if kind=='contact':
# No fake phone, email or working form without configured destination.
assert not doc.select_one('form[action=""]')
def test_new_product_theme_preserves_seo_and_archive_is_buildable(factory):
sid=site(factory)
for name in THEMES:
for version in ('1.2.0','1.3.0'):
selected=factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':name,'version':version})
assert selected.status_code==200,selected.text
built=factory.post(f'/api/sites/{sid}/builds',json={})
assert built.status_code==200,built.text
bid=built.json()['version']
page=factory.get(f'/preview/{sid}/{bid}/')
assert page.status_code==200
css=factory.get(f'/preview/{sid}/{bid}/assets/pro.css').text
assert ('V8.30 REAL PAGE COMPOSITION' in css)==(version=='1.3.0')
assert '<meta name="robots"' in page.text
+64
View File
@@ -0,0 +1,64 @@
"""Real static build + legacy pin smoke for performance and semantic icons."""
from pathlib import Path
from bs4 import BeautifulSoup
from app import db, theme_store
from test_factory_v2 import factory,site
def test_latest_theme_icons_and_preview_resources(factory):
assert theme_store.latest('automotive')=='1.4.1'
for kind in ('home','product','brand','listing','contact'):
result=factory.get('/api/factory/themes/automotive/preview',params={'page_type':kind})
assert result.status_code==200,result.text[:400]
doc=BeautifulSoup(result.text,'html.parser')
assert len(doc.select('link[rel=stylesheet]'))==1
assert len(doc.select('script[src]'))==1
assert 'preview.bundle.css' in doc.select_one('link[rel=stylesheet]')['href']
assert 'preview.bundle.js' in doc.select_one('script[src]')['src']
uses=doc.select('svg.auto-icon use[href]')
assert uses
assert all('preview.icons.svg' in v['href'] for v in uses)
for name in ('bundle.css','bundle.js','icons.svg'):
r=factory.get('/api/factory/themes/automotive/preview.'+name)
assert r.status_code==200,(name,r.text[:80])
assert len(r.content)>250
def test_compiled_assets_in_new_version_and_legacy_original(factory):
sid=site(factory)
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code==200
for version in ('1.4.0','1.4.1'):
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'automotive','version':version}).status_code==200
r=factory.post(f'/api/sites/{sid}/builds',json={})
assert r.status_code==200,r.text[:400]
bid=r.json()['version']
def open_path(path):
reply=factory.get(f'/preview/{sid}/{bid}/'+path)
assert reply.status_code==200,(version,path,reply.text[:160])
return reply
home=BeautifulSoup(open_path('').text,'html.parser')
if version=='1.4.1':
css=home.select('link[rel=stylesheet]')
js=home.select('script[src]')
assert len(css)==1 and css[0]['href']=='assets/auto-compiled.css'
assert len(js)==1 and js[0]['src']=='assets/auto-compiled.js'
icons=open_path('assets/auto-icons.svg').text
assert '<symbol id="search"' in icons and '<symbol id="chat"' in icons
assert '.auto31-search' in open_path('assets/auto-compiled.css').text
script=open_path('assets/auto-compiled.js').text
assert 'data-pro-menu' in script and 'search-index.json' in script
assert all(icon['href'].startswith('assets/auto-icons.svg#') for icon in home.select('use[href]'))
assert 'auto-compiled' in open_path('contact/').text
assert open_path('products/').status_code==200
else:
assert len(home.select('link[rel=stylesheet]'))>=6
assert len(home.select('script[src]'))>=3
assert 'auto-compiled.css' not in str(home)
def test_old_theme_source_is_snapshotted():
old=theme_store.resolve('automotive','1.4.0')
new=theme_store.resolve('automotive','1.4.1')
assert (old/'pro.html').read_text()!= (new/'pro.html').read_text()
assert 'auto-icons.svg' not in (old/'pro.html').read_text()
assert theme_store.compatibility('automotive','1.4.0')['valid']
+79
View File
@@ -0,0 +1,79 @@
"""Automotive v1.4.0 information architecture inspired by Magic Bridge Auto.
No real external content or vehicle claim is imported by the theme.
"""
import json
from pathlib import Path
from bs4 import BeautifulSoup
from app import theme_store
from test_factory_v2 import factory, site
ROOT=Path(__file__).resolve().parents[1]
def test_automotive_new_and_old_versions_remain_available(factory):
assert theme_store.latest('automotive')=='1.4.1'
for version in ('1.4.0','1.3.0','1.2.0','1.1.0','1.0.0'):
p=theme_store.resolve('automotive',version)
assert theme_store.compatibility('automotive',version)['valid']
assert ('auto31-home' in (p/'pages/home.html').read_text()) == (version == '1.4.0')
assert "'pages/' ~ kind" in (p/'pro.html').read_text()
assert theme_store.latest('industrial')=='1.3.0'
assert 'auto31-home' in (theme_store.resolve('automotive','1.4.0')/'pages/home.html').read_text()
def test_reference_inspired_full_page_structure(factory):
checks={'home':('.auto31-home','.auto31-search','.auto31-feature-tile'),
'listing':('.auto31-catalog','.pro-filter-panel','.pro-filter-scope'),
'brand':('.auto31-brand','#auto31-brand-models','.auto31-brand-story'),
'product':('.auto31-detail','.auto31-detail-main','.auto31-spec-panel'),
'about':('.pro-v30-about',), 'contact':('.pro-v30-contact',)}
for kind,selectors in checks.items():
res=factory.get('/api/factory/themes/automotive/preview',params={'page_type':kind})
assert res.status_code==200,(kind,res.text[:350])
soup=BeautifulSoup(res.text,'html.parser')
assert len(soup.select('h1'))==1
assert all(soup.select_one(css) for css in selectors),(kind,selectors)
assert soup.select_one('[data-pro-menu]')
def test_automotive_search_and_mega_use_real_catalogue_data(factory):
sid=site(factory)
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code==200
product=factory.post(f'/api/sites/{sid}/entries',json={
'type':'product','title':'Fictional A1','route':'/products/fictional-a1/',
'summary':'Demonstration model, not real inventory',
'meta':{'fields':{'brand':'DemoMotors','category':'SUV','energy':'EV'}}})
assert product.status_code==201,product.text
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'automotive','version':'1.4.0'}).status_code==200
build=factory.post(f'/api/sites/{sid}/builds',json={})
assert build.status_code==200,build.text
version=build.json()['version']
def view(route):
r=factory.get(f'/preview/{sid}/{version}{route}')
assert r.status_code==200,(route,r.text[:250])
return r.text
home=view('/')
assert 'DemoMotors' in home and 'Fictional A1' in home
assert 'auto31.js' in home and 'auto31-search' in home
catalogue=view('/products/')
assert 'Fictional A1' in catalogue
assert 'data-pro-filter-search' in catalogue
assert 'DemoMotors' in view('/brands/demomotors/')
detail=view('/products/fictional-a1/')
assert 'Vehicle' in detail or '车辆参数' in detail
assert '<meta name="robots"' in detail
assert factory.get(f'/preview/{sid}/{version}/assets/auto31.js').status_code==200
report=factory.get(f'/api/sites/{sid}/builds/{version}/report').json()
assert not report['errors']
def test_v830_archived_template_is_not_v831_home(factory):
sid=site(factory)
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code==200
for version in ('1.3.0','1.4.0','1.4.1'):
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':'automotive','version':version}).status_code==200
build=factory.post(f'/api/sites/{sid}/builds',json={})
assert build.status_code==200,build.text
p=factory.get(f'/preview/{sid}/{build.json()["version"]}/')
assert p.status_code==200
assert ('auto31-home' in p.text)==(version in ('1.4.0','1.4.1'))
assert ('auto31-hero-grid' in p.text)==(version in ('1.4.0','1.4.1'))
+1 -1
View File
@@ -9,7 +9,7 @@ def test_default_workspace_has_guided_navigation(client):
page=client.get('/')
assert page.status_code==200
for label in ('工作台','我的网站','网站概览','内容与页面','外观与设计',
'导入与迁移','构建与交付','网站设置','高级工具'):
'导入与迁移','生成与预览','检查并上线','网站设置','高级工具'):
assert label in page.text
assert '/ui/workspace.js' in page.text
assert '/ui/workspace.css' in page.text
+3 -3
View File
@@ -47,9 +47,9 @@ def test_safe_import_is_default_and_direct_import_is_explicit(client):
def test_wizard_does_not_claim_design_automatically_finished(client):
js=client.get('/ui/workspace.js').text
assert 'optional:true,view:\'design\'' in js
assert '不自动标记为完成' not in js or '设计可以随时修改' in js
js=client.get('/ui/workspace-guide.js').text
assert "id:'design'" in js and 'optional:true' in js
assert "done:false,optional:true" in js
def test_live_job_endpoint_can_list_user_site_jobs(client):