Files
probably-rendering-one-more…/apps/studio/scripts/infinite-canvas-smoke.cjs
codex-bot f1c794654e
creative workspace review checks / verify (pull_request) Has been cancelled
Edit canvas text and replace images directly on double click
2026-10-30 14:32:32 +08:00

92 lines
9.6 KiB
JavaScript

// Supply authorized browser state for the server. Local acceptance was explicitly authorized for this revision.
const {chromium}=require('playwright-core');
const assert=require('node:assert/strict');
const fs=require('node:fs/promises');
(async()=>{
const base=process.env.BASE_URL;
assert.ok(['http://8.211.154.134:18131','http://127.0.0.1:3024'].includes(base),'Use the designated server or the explicitly authorized local preview');
const browser=await chromium.launch(process.env.CHROMIUM_PATH?{executablePath:process.env.CHROMIUM_PATH,headless:true}:{channel:'chrome',headless:true});
try{
const context=await browser.newContext({viewport:{width:1600,height:1100},storageState:process.env.STORAGE_STATE||undefined});
const page=await context.newPage(),errors=[];page.on('pageerror',e=>errors.push(e.message));
await page.goto(base+'/infinite_canvas');
assert.equal(new URL(page.url()).pathname,'/infinite_canvas','Login required; authenticate without weakening protection');
await page.getByRole('heading',{name:'无限画布 / Infinite canvas'}).waitFor();
await page.getByRole('button',{name:'从示例开始 ↗',exact:true}).click();
// A fresh context is used so this test cannot overwrite a human browser draft.
const snapshot=()=>page.evaluate(()=>JSON.parse(localStorage.getItem('studio.infinite-canvas.v1')));
const node=id=>page.locator(`[data-node-id="${id}"]`);
const close=(a,b)=>assert.ok(Math.abs(a-b)<2,`${a} ≈ ${b}`);
async function drag(handle,dx,dy){const box=await handle.boundingBox();assert.ok(box);await page.mouse.move(box.x+box.width/2,box.y+box.height/2);await page.mouse.down();await page.mouse.move(box.x+box.width/2+dx,box.y+box.height/2+dy,{steps:12});await page.mouse.up()}
for(const scale of [0,1]){
await page.getByRole('button',{name:'适应画布',exact:true}).click();
if(scale)await page.getByRole('button',{name:'放大',exact:true}).click();
const z=await page.locator('[class*=world]').evaluate(el=>new DOMMatrix(getComputedStyle(el).transform).a);
for(const id of ['brief','reference']){
const before=(await snapshot()).cards.find(c=>c.id===id);
await drag(node(id).locator(':scope > div').nth(1),45,25);
const after=(await snapshot()).cards.find(c=>c.id===id);close(after.x-before.x,45/z);close(after.y-before.y,25/z);
}
}
// Double-click content enters editing without moving or creating nodes.
const nodeCount=(await snapshot()).cards.length;
const before=(await snapshot()).cards.find(c=>c.id==='brief');
await node('brief').locator('textarea').dblclick({force:true});
assert.equal(await node('brief').locator('textarea').getAttribute('readonly'),null);
await node('brief').locator('textarea').fill('双击后直接编辑');
assert.equal((await snapshot()).cards.length,nodeCount);
assert.equal((await snapshot()).cards.find(c=>c.id==='brief').x,before.x);
assert.equal((await snapshot()).cards.find(c=>c.id==='brief').y,before.y);
await page.keyboard.press('Escape');
const chooser=page.waitForEvent('filechooser');
await node('reference').getByRole('button',{name:/替换.*图片/}).dblclick();
const imagePicker=await chooser;
await imagePicker.setFiles({name:'replacement.png',mimeType:'image/png',buffer:Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jC1sAAAAASUVORK5CYII=','base64')});
await page.waitForFunction(()=>JSON.parse(localStorage.getItem('studio.infinite-canvas.v1')).cards.find(c=>c.id==='reference').image?.startsWith('data:image/png;base64,'));
assert.equal((await snapshot()).cards.length,nodeCount);
await page.keyboard.press('Escape');
assert.equal(await node('brief').locator('textarea').getAttribute('readonly'),'');await node('brief').getByRole('button',{name:/^编辑 /}).click();await node('brief').locator('textarea').fill('真实工具内编辑 · 输入不能移动节点');
const edited=(await snapshot()).cards.find(c=>c.id==='brief');assert.equal(edited.x,before.x);assert.equal(edited.y,before.y);
await drag(node('brief').locator('textarea'),20,10);const inEdit=(await snapshot()).cards.find(c=>c.id==='brief');assert.equal(inEdit.x,edited.x);assert.equal(inEdit.y,edited.y);await node('brief').getByRole('button',{name:/^完成编辑 /}).click();const resize=node('brief').getByRole('button',{name:/调整/});await drag(resize,60,40);
assert.ok((await snapshot()).cards.find(c=>c.id==='brief').width>320);
await page.getByRole('button',{name:'撤销',exact:true}).click();assert.equal((await snapshot()).cards.find(c=>c.id==='brief').width,undefined);
await page.getByRole('button',{name:'重做',exact:true}).click();assert.ok((await snapshot()).cards.find(c=>c.id==='brief').width>320);
// Each SVG endpoint must agree with the actual transformed node edge, after resize and pan.
async function wires(){const mismatch=await page.evaluate(()=>{
return [...document.querySelectorAll('svg path')].filter(p=>p.getAttribute('d')?.startsWith('M ')).slice(0,1).some(p=>{const d=p.getAttribute('d').match(/-?\d+(?:\.\d+)?/g).map(Number),world=p.closest('div').getBoundingClientRect();const matrix=p.getScreenCTM();const pt=new DOMPoint(d[0],d[1]).matrixTransform(matrix);const card=document.querySelector('[data-node-id="brief"]').getBoundingClientRect();return Math.abs(pt.x-card.right)>3||Math.abs(pt.y-(card.top+card.height/2))>3});
});assert.equal(mismatch,false)}
// The first link starts at brief; check it specifically.
await wires();
await page.getByRole('button',{name:'移动画布',exact:true}).click();
const viewport=page.locator('[class*="viewport"]');const box=await viewport.boundingBox();await page.mouse.move(box.x+20,box.y+20);await page.mouse.down();await page.mouse.move(box.x+100,box.y+80,{steps:8});await page.mouse.up();
await wires();
const saved=await snapshot(),view=await page.evaluate(()=>localStorage.getItem('studio.infinite-canvas.v1.view'));
await page.reload();await page.getByRole('heading',{name:'无限画布 / Infinite canvas'}).waitFor();assert.deepEqual(await snapshot(),saved);assert.equal(await page.evaluate(()=>localStorage.getItem('studio.infinite-canvas.v1.view')),view);
// Zoom must preserve the world coordinate beneath the pointer.
const vp=await viewport.boundingBox();assert.equal(vp.x,0);assert.equal(vp.y,0);assert.equal(vp.width,1600);assert.equal(vp.height,1100);
const anchor={x:vp.x+vp.width*.65,y:vp.y+vp.height*.25};
const worldAt=()=>page.locator('[class*=world]').evaluate((el,p)=>{const m=new DOMMatrix(getComputedStyle(el).transform);return {x:(p.x-m.e)/m.a,y:(p.y-m.f)/m.d}},anchor);
const point=await worldAt();await page.mouse.move(anchor.x,anchor.y);await page.keyboard.down('Control');await page.mouse.wheel(0,-80);await page.keyboard.up('Control');
await page.waitForFunction(old=>JSON.parse(localStorage.getItem('studio.infinite-canvas.v1.view')).zoom!==JSON.parse(old).zoom,view);
const pointAfter=await worldAt();close(point.x,pointAfter.x);close(point.y,pointAfter.y);
await page.getByRole('button',{name:'选择工具',exact:true}).click();
await page.getByRole('button',{name:'添加笔记',exact:true}).click();
assert.equal((await snapshot()).cards.length,saved.cards.length+1);
await page.getByRole('button',{name:/操作$/}).last().click();
await page.getByRole('button',{name:'复制节点',exact:true}).click();assert.equal((await snapshot()).cards.length,saved.cards.length+2);
await page.getByRole('button',{name:'删除节点',exact:true}).click();assert.equal((await snapshot()).cards.length,saved.cards.length+1);
await page.getByRole('button',{name:'撤销',exact:true}).click();assert.equal((await snapshot()).cards.length,saved.cards.length+2);
await page.getByRole('button',{name:'重做',exact:true}).click();assert.equal((await snapshot()).cards.length,saved.cards.length+1);
if(await page.getByRole('button',{name:'关闭画布操作',exact:true}).isVisible())await page.getByRole('button',{name:'关闭画布操作',exact:true}).click();
// Return to the saved three-node document through undo.
await page.getByRole('button',{name:'撤销',exact:true}).click();await page.getByRole('button',{name:'撤销',exact:true}).click();await page.getByRole('button',{name:'撤销',exact:true}).click();
await page.getByRole('button',{name:'适应画布',exact:true}).click();await page.reload();await page.getByRole('heading',{name:'无限画布 / Infinite canvas'}).waitFor();
const dock=await page.getByRole('navigation',{name:'画布工具'}).boundingBox();assert.ok(dock.y>950);assert.ok(dock.x>500);
await page.getByRole('button',{name:'切换明暗主题'}).click();assert.equal(await page.locator('main').getAttribute('data-theme'),'light');await page.reload();await page.waitForFunction(()=>document.querySelector('main').dataset.theme==='light');
if(process.env.SCREENSHOT_DIR)await page.screenshot({path:process.env.SCREENSHOT_DIR+'/canvas-light.png'});
await page.getByRole('button',{name:'切换明暗主题'}).click();
if(process.env.SCREENSHOT_DIR){await fs.mkdir(process.env.SCREENSHOT_DIR,{recursive:true});await page.screenshot({path:process.env.SCREENSHOT_DIR+'/canvas-rebuild.png'});await page.setViewportSize({width:390,height:844});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));const mobile=await viewport.boundingBox();assert.equal(mobile.width,390);assert.equal(mobile.height,844);await page.getByRole('button',{name:'适应画布',exact:true}).click();await page.screenshot({path:process.env.SCREENSHOT_DIR+'/canvas-mobile.png'});}
assert.deepEqual(errors,[]);console.log('Passed: two-node drag at two scales, inline input, resize, undo/redo, wires, pan, reload, pointer zoom, CRUD and full-screen desktop/mobile');
}finally{await browser.close()}
})().catch(e=>{console.error(e);process.exitCode=1});