test: package HEL-806 evidence and verify genuine empty state
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -6,7 +6,7 @@ The approved HTML remains read-only, SHA256 `0473d80d22b90194375f3566c1b8ca30dbd
|
||||
- `StudioDesk.svelte`, `Artboard.svelte`: replace the extra 12px gap with the original stage padding (12px 16px 8px, top 8px when height <=800), keep the 40px action row for empty as well as populated states, and clip media to its theme radius (8px night/day, 4px grade, 14px bluehour). Values have one source in tokens; no overlay stylesheet or duplicate component.
|
||||
- `e2e/hel806.cjs`: real disposable HTTP data, fake ComfyUI/model only, four themes × three viewports. Original reference, runtime content map and actual screenshots are distinct. Reference changes use existing templates/content/state; no reference CSS/layout changes. Missing empty states, non-storyboard stage content, real edit/compare/validation dialogs and organization-only pages are explicitly marked. The sample's static parameter list is not substituted for actual workflow declarations.
|
||||
- `e2e/hel806-toast.mjs`, `hel806-toast-browser.cjs`: deterministic expiry/pause/sticky/cleanup regression plus full/reduced motion, project footer clearance and real explicit confirmation.
|
||||
- `scripts/report-hel806.py`: measured values, self-contained themed galleries and parallel images. Screenshot delivery uses lossless WebP; decoded RGB equality is checked against source PNGs. Manifests retain both hashes. Screenshot counts and DOM measurements are evidence, never automatic visual approval.
|
||||
- `scripts/report-hel806.py`: measured values, self-contained themed galleries and parallel images. Screenshot delivery uses lossless WebP; decoded RGB equality is checked against source PNGs. Manifests retain both hashes. Identical bitmap bytes are deduplicated inside self-contained upload parts; media-files.json preserves the logical-name mapping, and every state stays with its three viewport sizes. Screenshot counts and DOM measurements are evidence, never automatic visual approval.
|
||||
|
||||
Reproduce from a frozen build: install the repository's existing dependencies, run frontend lint/check/test/build, then set existing `PLAYWRIGHT_MODULE_PATH`, browser library path and `HEL732_PYTHON` for this machine. `HEL733_TESTS=e2e/hel732.cjs,e2e/hel729.cjs,e2e/hel742.cjs,e2e/hel733-live.cjs,e2e/hel732-visual.cjs,e2e/hel806.cjs,e2e/hel806-toast-browser.cjs HEL733_SKIP_SHOTS=1 python3 e2e/run-hel733.py`. The original HTML is downloaded through the CLI to `../materials` and the B4 reference copy to `../evidence/reference`. All required results are collected before exit; the runner closes its exact child processes and removes the disposable database. Run `node e2e/hel806-toast.mjs` separately. The report script reads `../evidence/hel806-final/results.json`.
|
||||
|
||||
|
||||
+4
-1
@@ -37,6 +37,9 @@ const metrics=(p,sels)=>p.evaluate(sels=>Object.fromEntries(Object.entries(sels)
|
||||
if(process.env.HEL806_SEED_TASKS==='1'){
|
||||
const seeded=[];for(const mode of ['ok','fail']){fs.writeFileSync('e2e/fake_mode.json',JSON.stringify({mode,delay_sec:0,unique_filename:true,image_file:path.resolve('calliope-web/static/refract/coast.jpg')}));const created=await api('/api/playground/generate',{workflow_id:f.dual,source:'studio-image',input_values:{'1':'HEL806 isolated '+mode,'2':'模糊','3':42}});let job;for(let i=0;i<200;i++){job=await api('/api/jobs/'+created.job.id);if(!['running','pending'].includes(job.status))break;await wait(150)}assert.equal(job.status,mode==='ok'?'done':'failed');seeded.push(job)}fs.writeFileSync(OUT+'/seed-jobs.json',JSON.stringify(seeded,null,2));
|
||||
}
|
||||
if(process.env.HEL806_REAL_EMPTY==='1'){
|
||||
assert.equal((await api('/api/jobs')).length,0,'empty-state run must start without jobs');const owned=await api('/api/projects');for(const project of owned){const r=await fetch(API+'/api/projects/'+project.id,{method:'DELETE'});assert(r.ok,'delete only disposable fixture project')};const after={projects:await api('/api/projects'),jobs:await api('/api/jobs')};assert.equal(after.projects.length,0);assert.equal(after.jobs.length,0);fs.writeFileSync(OUT+'/real-empty.json',JSON.stringify({removedFixtureIds:owned.map(p=>p.id),after},null,2));
|
||||
}
|
||||
const sample=http.createServer((req,res)=>{res.setHeader('Content-Type','text/html;charset=utf-8');res.end(fs.readFileSync('../materials/夜片场-多主题交互样本.html'))});await new Promise(r=>sample.listen(0,'127.0.0.1',r));
|
||||
const browser=await chromium.launch({args:['--no-sandbox','--disable-dev-shm-usage']});
|
||||
try{
|
||||
@@ -55,7 +58,7 @@ const metrics=(p,sels)=>p.evaluate(sels=>Object.fromEntries(Object.entries(sels)
|
||||
{name:'video-output',ref:'create',run:()=>generate(videoWf.id,'video'),creative:true},
|
||||
{name:'image-failed',ref:'create',run:async()=>{await creative(f.dual);await p.locator('#desk-prompt').fill('HEL806 fake failure');fs.writeFileSync('e2e/fake_mode.json',JSON.stringify({mode:'fail',delay_sec:0}));await p.locator('.gen').click();await p.locator('.stage-empty').filter({hasText:'失败'}).waitFor({timeout:45000})},creative:true},
|
||||
{name:'studio-real',ref:'studio',run:()=>goto('/'),basis:'approved-cover-exception'},
|
||||
{name:'studio-empty',ref:'studio',run:()=>emptyRoute('/','**/api/projects',[]),missing:'原件没有无项目空态'},
|
||||
{name:'studio-empty',ref:'studio',run:async()=>{if(process.env.HEL806_REAL_EMPTY==='1')await goto('/');else{await p.route('**/api/jobs',r=>r.fulfill({json:[]}));await emptyRoute('/','**/api/projects',[])}},missing:'原件没有无项目空态'},
|
||||
{name:'image-text-empty',ref:'create',run:async()=>{await p.route('**/api/playground/jobs',r=>r.fulfill({json:[]}));await creative(f.dual)},creative:true},
|
||||
{name:'image-long-prompt',ref:'create',run:async()=>{await creative(f.dual);await p.locator('#desk-prompt').fill('海边女孩、晨光、纸船与灯塔,保持人物和空间连续。'.repeat(70));const btn=p.getByRole('button',{name:'展开提示词',exact:true});if(await btn.count())await btn.click()},creative:true},
|
||||
{name:'image-edit-upload',ref:'create',run:async()=>{await creative(edit.id);await upload()},creative:true},
|
||||
|
||||
+36
-12
@@ -44,18 +44,42 @@ with (REPORT/'key-values.csv').open('w') as f:
|
||||
values=lambda o:[round(o[k],4) for k in ['x','y','width','height']]
|
||||
av,bv=values(a),values(b);w.writerow([r['name'],r['theme'],f"{r['width']}x{r['height']}",key,av,bv,[round(x-y,4)for x,y in zip(av,bv)],r['basis']])
|
||||
(REPORT/'capture-manifest.json').write_text(json.dumps({'source_png_to_lossless_webp':manifest,'states':len({r['name']for r in rows}),'groups':len(rows),'errors':data['errors'],'blocked_external_origins':sorted(set(data['blocked']))},indent=2))
|
||||
# A theme archive is self-contained and opens without a running server.
|
||||
# Self-contained parts keep every state/three sizes together and each upload under 40 MB.
|
||||
archives=[]
|
||||
for theme in ['night','day','grade','bluehour']:
|
||||
theme_rows=[r for r in rows if r['theme']==theme]
|
||||
intro='<meta charset="utf-8"><title>HEL806 '+theme+'</title><style>body{font:15px system-ui;background:#181818;color:#eee}img{max-width:100%}a{color:#8ed3ff}details{margin:20px 0}</style><h1>HEL806 '+theme+'</h1><p>原图只读。mapped只映射内容;missing-reference 表示原件缺此状态。并排图左为映射参考,右为隔离实图,不代表全页通过。原始数值在results.json。</p>'
|
||||
chunks=[intro]
|
||||
for r in theme_rows:
|
||||
stem=f"{r['name']}-{theme}-{r['width']}"
|
||||
chunks.append('<details><summary>'+html.escape(stem+' · '+r['basis']+' · '+str(r.get('missing','')))+'</summary>'+f'<a href="shots/{stem}-original.webp">未改原图</a> · <a href="shots/{stem}-mapped.webp">内容映射参考</a> · <a href="shots/{stem}-actual.webp">实图</a><img loading="lazy" src="shots/{stem}-pair.webp"></details>')
|
||||
with zipfile.ZipFile(REPORT/f'HEL806-{theme}.zip','w',zipfile.ZIP_DEFLATED,compresslevel=3) as z:
|
||||
z.writestr('index.html','\n'.join(chunks));z.writestr('results.json',json.dumps(theme_rows,ensure_ascii=False,indent=2))
|
||||
for r in theme_rows:
|
||||
b4files=[p for p in (ROOT/'e2e/shots/hel732').glob(f'*-{theme}-*.png') if p.name.startswith(('home-','task-'))]
|
||||
b4size=sum(p.stat().st_size for p in b4files)
|
||||
chunks=[];current=[];objects={};weight=b4size
|
||||
def files_for(group):
|
||||
return [OUT/'shots'/f"{r['name']}-{theme}-{r['width']}-{suffix}.webp" for r in group for suffix in ['original','mapped','actual','pair']]
|
||||
for name in dict.fromkeys(r['name'] for r in theme_rows):
|
||||
group=[r for r in theme_rows if r['name']==name]
|
||||
candidates={hashlib.sha256(p.read_bytes()).hexdigest():p for p in files_for(group)}
|
||||
extra=sum(p.stat().st_size for h,p in candidates.items() if h not in objects)
|
||||
if current and weight+extra>40_000_000:
|
||||
chunks.append(current);current=[];objects={};weight=0
|
||||
extra=sum(p.stat().st_size for p in candidates.values())
|
||||
current+=group;objects.update(candidates);weight+=extra
|
||||
if current:chunks.append(current)
|
||||
for part,part_rows in enumerate(chunks,1):
|
||||
media={};objects={}
|
||||
for p in files_for(part_rows):
|
||||
h=hashlib.sha256(p.read_bytes()).hexdigest();media[p.name]='assets/'+h+'.webp';objects[h]=p
|
||||
doc=['<meta charset="utf-8"><title>HEL806 '+theme+'</title><style>body{font:15px system-ui;background:#181818;color:#eee}img{max-width:100%}a{color:#8ed3ff}details{margin:20px 0}</style><h1>HEL806 '+theme+' / '+str(part)+'</h1><p>原图只读;mapped只映射内容。missing-reference 是缺依据;并排左参考、右隔离实图,不代表全页通过。此分包可独立解压阅读。B4 gallery 位于本主题 part1 的 B4-index.html</p>']
|
||||
for r in part_rows:
|
||||
stem=f"{r['name']}-{theme}-{r['width']}"
|
||||
for suffix in ['original','mapped','actual','pair']:
|
||||
p=OUT/'shots'/f'{stem}-{suffix}.webp';z.write(p,'shots/'+p.name)
|
||||
print(json.dumps({'groups':len(rows),'states':len({r['name']for r in rows}),'archives':{p.name:p.stat().st_size for p in REPORT.glob('*.zip')}},indent=2))
|
||||
link=lambda suffix:media[stem+'-'+suffix+'.webp']
|
||||
doc.append('<details><summary>'+html.escape(stem+' · '+r['basis']+' · '+str(r.get('missing','')))+'</summary>'+f'<a href="{link("original")}">未改原图</a> · <a href="{link("mapped")}">内容映射参考</a> · <a href="{link("actual")}">实图</a><img loading="lazy" src="{link("pair")}"></details>')
|
||||
target=REPORT/f'HEL806-{theme}-part{part}.zip'
|
||||
with zipfile.ZipFile(target,'w',zipfile.ZIP_DEFLATED,compresslevel=3) as z:
|
||||
z.writestr('index.html','\n'.join(doc));z.writestr('results.json',json.dumps(part_rows,ensure_ascii=False,indent=2));z.writestr('media-files.json',json.dumps(media,indent=2))
|
||||
for h,p in objects.items():z.write(p,'assets/'+h+'.webp')
|
||||
if part==1:
|
||||
b4doc=['<meta charset="utf-8"><h1>B4 '+theme+'</h1><p>沿用已批准内容/hero映射。原PNG保持不变;不把获批例外扩大解释。</p>']
|
||||
for p in sorted(b4files):z.write(p,'b4/'+p.name);b4doc.append('<p>'+html.escape(p.name)+'</p><img style="max-width:100%" loading="lazy" src="b4/'+p.name+'">')
|
||||
z.writestr('B4-index.html','\n'.join(b4doc))
|
||||
assert target.stat().st_size<45_000_000
|
||||
archives.append({'name':target.name,'bytes':target.stat().st_size,'groups':len(part_rows)})
|
||||
(REPORT/'archives.json').write_text(json.dumps(archives,indent=2))
|
||||
print(json.dumps({'groups':len(rows),'states':len({r['name']for r in rows}),'archives':archives},indent=2))
|
||||
|
||||
Reference in New Issue
Block a user