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:
@@ -0,0 +1,51 @@
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const H=require('../app/static/editor-helpers.js');
|
||||
function sample(){return [
|
||||
{id:'root1111',type:'layout',columns:[
|
||||
{width:1,blocks:[{id:'inner2222',type:'layout',gap:20,columns:[
|
||||
{width:1,blocks:[{id:'deep3333',type:'image_text',title:'Original',description:'Text'}]},
|
||||
{width:1,blocks:[{id:'deep4444',type:'cta',title:'CTA'}]}]}]},
|
||||
{width:1,blocks:[{id:'shallow5',type:'features',title:'Features'}]}
|
||||
]},
|
||||
{id:'otherRoot',type:'layout',columns:[{width:1,blocks:[{id:'foreign6',type:'cta',title:'Other'}]}]}
|
||||
]}
|
||||
const types={image_text:{fields:['title','description']},cta:{fields:['title']},layout:{fields:['columns']},features:{fields:['title']}};
|
||||
test('locate second-level block by immediate parent and id, not client-supplied indexes',()=>{
|
||||
let rows=sample();let f=H.findNestedChild(rows,'inner2222','deep3333');
|
||||
assert.equal(f.parentIndex,0);assert.equal(f.depth,1);assert.equal(f.parent.id,'inner2222');
|
||||
assert.equal(f.child.title,'Original');assert.deepEqual(f.ancestors,['root1111','inner2222']);
|
||||
assert.equal(H.findNestedChild(rows,'root1111','deep3333'),null);
|
||||
assert.equal(H.findNestedChild(rows,'otherRoot','deep3333'),null);
|
||||
assert.equal(H.findChildInRoot(rows,'root1111','deep3333').child.id,'deep3333');
|
||||
assert.equal(H.findChildInRoot(rows,'otherRoot','deep3333'),null);
|
||||
});
|
||||
test('only allow safe operations and supported inline text changes',()=>{
|
||||
const rows=sample();const ok={action:'child-inline-text',blockId:'inner2222',childId:'deep3333',field:'title',value:'Safe title'};
|
||||
assert.equal(H.safeNestedCanvasCommand(ok,rows,types),true);
|
||||
assert.equal(H.safeNestedCanvasCommand({...ok,field:'body'},rows,types),false);
|
||||
assert.equal(H.safeNestedCanvasCommand({...ok,field:'title',value:'x'.repeat(501)},rows,types),false);
|
||||
assert.equal(H.safeNestedCanvasCommand({...ok,blockId:'root1111'},rows,types),false);
|
||||
assert.equal(H.safeNestedCanvasCommand({...ok,action:'child-script'},rows,types),false);
|
||||
assert.equal(H.safeNestedCanvasCommand({...ok,childId:'foreign6'},rows,types),false);
|
||||
});
|
||||
test('nested layout resolution preserves site/page boundaries and width constraints',()=>{
|
||||
let rows=sample();let inner=H.findLayout(rows,'inner2222');
|
||||
assert.equal(inner.rootIndex,0);assert.equal(inner.depth,1);
|
||||
assert.equal(H.findLayout(rows,'shallow5'),null);
|
||||
assert.deepEqual(H.layoutPreset(inner.layout,'left-wide'),[2,1]);
|
||||
assert.deepEqual(H.adjacentWidths(inner.layout,[2,1]),[2,1]);
|
||||
assert.equal(H.adjacentWidths(inner.layout,[.1,1]),null);
|
||||
assert.equal(H.findLayout(rows,'nonexistent'),null);
|
||||
assert.equal(H.findLayout(rows,'otherRoot').rootIndex,1);
|
||||
});
|
||||
test('duplication rewrites all nested IDs and reordering is scoped',()=>{
|
||||
const rows=sample(),seen=[];
|
||||
const copy=H.duplicate(rows[0].columns[0].blocks[0],()=>('copy'+(++seen.length)).padEnd(8,'z'));
|
||||
assert.equal(copy.type,'layout');
|
||||
assert.notEqual(copy.id,rows[0].columns[0].blocks[0].id);
|
||||
assert.notEqual(copy.columns[0].blocks[0].id,'deep3333');
|
||||
const original=rows[0].columns[0].blocks[0].columns[0].blocks;
|
||||
assert.equal(H.moveNested(rows[0].columns[0].blocks[0],0,0,-1),null);
|
||||
assert.equal(original[0].id,'deep3333');
|
||||
});
|
||||
@@ -42,3 +42,35 @@ test('numeric style input is clamped',()=>{
|
||||
assert.equal(clampNumber(-5,0,120,10),0);
|
||||
assert.equal(clampNumber('not-number',0,120,10),10);
|
||||
});
|
||||
const {findTopIndex,reorderById,adjacentWidths,safeCanvasCommand}=require('../app/static/editor-helpers.js');
|
||||
test('canvas top-level ID commands reorder only known blocks',()=>{
|
||||
const rows=[{id:'one',type:'hero'},{id:'two',type:'layout',columns:[{width:1},{width:1}]},{id:'three',type:'cta'}];
|
||||
assert.equal(findTopIndex(rows,'three'),2);
|
||||
assert.deepEqual(reorderById(rows,'one','three').map(x=>x.id),['two','three','one']);
|
||||
assert.equal(reorderById(rows,'one','missing'),null);
|
||||
assert.deepEqual(rows.map(x=>x.id),['one','two','three']);
|
||||
});
|
||||
test('canvas validates width limits, list length and types',()=>{
|
||||
const layout={id:'L',type:'layout',columns:[{width:1},{width:1}]};
|
||||
assert.deepEqual(adjacentWidths(layout,[1.6,.4]),null);
|
||||
assert.deepEqual(adjacentWidths(layout,[1.6,.5]),[1.6,.5]);
|
||||
assert.equal(adjacentWidths(layout,[1]),null);
|
||||
assert.equal(adjacentWidths(layout,[1,Infinity]),null);
|
||||
assert.equal(adjacentWidths({type:'hero'},[1,1]),null);
|
||||
});
|
||||
test('canvas rejects invented block ids, actions and unsafe text operations',()=>{
|
||||
const rows=[{id:'hello',type:'hero'},{id:'cols',type:'layout',columns:[{width:1},{width:1}]}];
|
||||
const fields={hero:{fields:['title','description','image']},layout:{fields:['columns']}};
|
||||
const ok=(action,id='hello',rest={})=>safeCanvasCommand({action,blockId:id,...rest},rows,fields);
|
||||
assert.equal(ok('select'),true);
|
||||
assert.equal(ok('duplicate'),true);
|
||||
assert.equal(ok('delete','fake'),false);
|
||||
assert.equal(ok('execute'),false);
|
||||
assert.equal(ok('reorder','hello',{targetId:'cols'}),true);
|
||||
assert.equal(ok('reorder','hello',{targetId:'missing'}),false);
|
||||
assert.equal(ok('resize-columns','cols',{widths:[2,1]}),true);
|
||||
assert.equal(ok('resize-columns','cols',{widths:[5,1]}),false);
|
||||
assert.equal(ok('inline-text','hello',{field:'title',value:'test'}),true);
|
||||
assert.equal(ok('inline-text','hello',{field:'body',value:'<script>x</script>'}),false);
|
||||
assert.equal(ok('inline-text','hello',{field:'title',value:'x'.repeat(501)}),false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const ux=require('../app/static/studio-experience-v829.js');
|
||||
const blocks={hero:{name:'顶部横幅'},richtext:{name:'文字介绍'},collection:{name:'产品列表'},layout:{name:'多列布局'},faq:{name:'常见问题'},gallery:{name:'图片画廊'}};
|
||||
test('palette groups are useful to a beginner and preserve every block',()=>{
|
||||
const grouped=ux.groupBlocks(blocks);
|
||||
assert.deepEqual(Object.keys(grouped),['basic','visual','business','advanced']);
|
||||
assert.equal(grouped.visual.some(x=>x.key==='hero'),true);
|
||||
assert.equal(grouped.business.some(x=>x.key==='collection'),true);
|
||||
assert.equal(grouped.advanced.some(x=>x.key==='layout'),true);
|
||||
assert.equal(Object.values(grouped).flat().length,Object.keys(blocks).length);
|
||||
});
|
||||
test('palette query searches Chinese user facing names and avoids mutation',()=>{
|
||||
assert.deepEqual(ux.groupBlocks(blocks,'产品').business.map(x=>x.key),['collection']);
|
||||
assert.equal(Object.values(ux.groupBlocks(blocks,'not-found')).flat().length,0);
|
||||
assert.equal(blocks.collection.name,'产品列表');
|
||||
});
|
||||
test('theme preview path is limited to approved themes and page types',()=>{
|
||||
assert.equal(ux.previewUrl('automotive','product'),'/api/factory/themes/automotive/preview?page_type=product');
|
||||
assert.equal(ux.previewUrl('../admin','home'),null);
|
||||
assert.equal(ux.previewUrl('corporate','javascript:alert(1)'),null);
|
||||
assert.equal(ux.previewUrl('minimal','about'),'/api/factory/themes/minimal/preview?page_type=about');
|
||||
});
|
||||
|
||||
test('three visual style presets keep schema-safe values',()=>{
|
||||
for(const theme of ['automotive','industrial','corporate','minimal']){
|
||||
for(const preset of ['clean','soft','feature']){
|
||||
const style=ux.stylePreset(theme,preset);
|
||||
assert.match(style.background,/^#[a-f0-9]{6}$/i);
|
||||
assert.match(style.text_color,/^#[a-f0-9]{6}$/i);
|
||||
assert.equal(Number.isInteger(style.padding),true);
|
||||
assert.ok(style.padding>=0&&style.padding<=120);
|
||||
}
|
||||
}
|
||||
assert.equal(ux.stylePreset('minimal','<script>'),null);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const {plan}=require('../app/static/workspace-guide.js');
|
||||
const site={id:'s1',name:'Demo',mode:'new',domain:'',updated_at:'2026-01-01T00:00:00Z'};
|
||||
const starter={id:'start',route:'/',source:'starter',status:'published',updated_at:'2026-01-01T00:00:00Z'};
|
||||
const product={id:'p',route:'/p/',source:'manual',status:'published',updated_at:'2026-01-02T00:00:00Z'};
|
||||
test('empty site is not a completed journey',()=>{const p=plan({site,entries:[starter]});assert.equal(p.next.id,'content');assert.equal(p.completed,1);assert.equal(p.hasBuild,false);assert.equal(p.live,false)});
|
||||
test('manually edited starter homepage can count as content',()=>{const p=plan({site,entries:[{...starter,created_at:'2026-01-01T00:00:00Z',updated_at:'2026-01-02T00:00:00Z'}]});assert.equal(p.next.id,'build')});
|
||||
test('a draft never counts as prepared content',()=>{const p=plan({site,entries:[starter,{...product,status:'draft'}]});assert.equal(p.next.id,'content')});
|
||||
test('migration site goes to the import workflow',()=>{const p=plan({site:{...site,mode:'migration'},entries:[starter]});assert.equal(p.steps[1].view,'import')});
|
||||
test('content without a build stays in the build step',()=>{const p=plan({site,entries:[starter,product]});assert.equal(p.next.id,'build');assert.equal(p.completed,2)});
|
||||
test('completed static build does not mean online',()=>{const p=plan({site,entries:[starter,product],builds:[{status:'complete',created_at:'2026-01-03T00:00:00Z'}]});assert.equal(p.next.id,'review');assert.equal(p.live,false);assert.equal(p.completed,3)});
|
||||
test('later edits mark build as requiring review',()=>{const p=plan({site,entries:[starter,{...product,updated_at:'2026-01-05T00:00:00Z'}],builds:[{status:'done',created_at:'2026-01-03T00:00:00Z'}]});assert.equal(p.next.id,'build');assert.equal(p.changedAfterBuild,true)});
|
||||
test('running job cannot be treated as completed build',()=>{const p=plan({site,entries:[starter,product],jobs:[{state:'running'}]});assert.equal(p.anyRunning,true);assert.equal(p.steps[3].done,false)});
|
||||
test('only a production deployment record is reported as deployed',()=>{const base={site,entries:[starter,product],builds:[{status:'done',created_at:'2026-01-03T00:00:00Z'}]};assert.equal(plan({...base,delivery:{history:[{target:'staging',release_id:'r'}]}}).live,false);assert.equal(plan({...base,delivery:{history:[{target:'production',release_id:'r'}]}}).live,true)});
|
||||
test('no site returns safely empty model',()=>{const p=plan({});assert.equal(p.ready,false);assert.equal(p.next,null)});
|
||||
@@ -0,0 +1,18 @@
|
||||
const {test}=require('node:test');const assert=require('node:assert/strict');const {plan}=require('../app/static/workspace-launch.js');
|
||||
const site={id:'s1'},build={id:'b1',status:'complete'};
|
||||
const approved=[{build_id:'b1',state:'approved'}];
|
||||
const preflight={siteId:'s1',buildId:'b1',target:'production',ready:true,message:'通过'};
|
||||
const delivery={configured:{staging:true,production:true},history:[]};
|
||||
const base={site,build,requests:approved,requestsKnown:true,preflight,delivery,target:'production',previewChecked:true};
|
||||
test('no build cannot deploy',()=>assert.equal(plan({...base,build:null}).canDeploy,false));
|
||||
test('queued build cannot deploy',()=>assert.equal(plan({...base,build:{...build,status:'queued'}}).canDeploy,false));
|
||||
test('requires review before deploying',()=>assert.equal(plan({...base,previewChecked:false}).canDeploy,false));
|
||||
test('requires approval from server',()=>assert.equal(plan({...base,requests:[{build_id:'b1',state:'pending'}]}).canDeploy,false));
|
||||
test('no accidental cross-build approval',()=>assert.equal(plan({...base,requests:[{build_id:'b2',state:'approved'}]}).canDeploy,false));
|
||||
test('cannot use preflight from another site',()=>assert.equal(plan({...base,preflight:{...preflight,siteId:'s2'}}).canDeploy,false));
|
||||
test('cannot use preflight for different build',()=>assert.equal(plan({...base,preflight:{...preflight,buildId:'b2'}}).canDeploy,false));
|
||||
test('cannot reuse preflight across targets',()=>assert.equal(plan({...base,target:'staging'}).canDeploy,false));
|
||||
test('needs server configured deployment target',()=>assert.equal(plan({...base,delivery:{configured:{production:false}}}).canDeploy,false));
|
||||
test('happy path merely enables submit; not live claim',()=>{const result=plan(base);assert.equal(result.canDeploy,true);assert.equal(result.hasRemoteRecord,false);assert.match(result.remoteDescription,/尚无/)});
|
||||
test('remote record still demands public verification',()=>{const result=plan({...base,delivery:{...delivery,history:[{target:'production',build_id:'b1',release_id:'abcd'}]}});assert.equal(result.hasRemoteRecord,true);assert.match(result.remoteDescription,/访问确认/)});
|
||||
test('unknown approvals are not approved',()=>assert.equal(plan({...base,requests:null}).approval,'unknown'));
|
||||
Reference in New Issue
Block a user