// Isolated browser context against the designated deployed server only. const {chromium}=require('playwright-core'); const assert=require('node:assert/strict'); const fs=require('node:fs/promises'); (async()=>{ const base=process.env.BASE_URL||'http://8.211.154.134:18131'; assert.equal(base,'http://8.211.154.134:18131'); const browser=await chromium.launch(process.env.CHROMIUM_PATH?{executablePath:process.env.CHROMIUM_PATH,headless:true,args:['--no-sandbox','--disable-dev-shm-usage']}:{channel:'chrome',headless:true}); let page; try{ const context=await browser.newContext({viewport:{width:1600,height:1100},storageState:process.env.STORAGE_STATE||undefined}); page=await context.newPage();const errors=[];let choosers=0; page.on('pageerror',e=>errors.push(e.message));page.on('filechooser',()=>choosers++); const snapshot=()=>page.evaluate(()=>JSON.parse(localStorage.getItem('studio.infinite-canvas.v1'))); const node=id=>page.locator(`[data-node-id="${id}"]`); const content=id=>node(id).locator('[data-node-content]'); const waitField=(id,key,value)=>page.waitForFunction(({id,key,value})=>JSON.parse(localStorage.getItem('studio.infinite-canvas.v1')).cards.find(n=>n.id===id)?.[key]===value,{id,key,value}); const composer=page.getByRole('region',{name:'节点编辑器'}); const capture=async name=>{if(process.env.SCREENSHOT_DIR){await fs.mkdir(process.env.SCREENSHOT_DIR,{recursive:true});await page.screenshot({path:process.env.SCREENSHOT_DIR+'/'+name+'.png'})}}; async function unobstructed(id){const box=await node(id).boundingBox();assert.ok(box);for(const card of (await snapshot()).cards.filter(n=>n.id!==id)){const other=await node(card.id).boundingBox();assert.ok(other);assert.equal(box.xother.x&&box.yother.y,false,`${id} overlaps ${card.id}`)}} const close=(a,b)=>assert.ok(Math.abs(a-b)<2,`${a} ≈ ${b}`); async function drag(handle,dx,dy){const b=await handle.boundingBox();assert.ok(b);await page.mouse.move(b.x+b.width/2,b.y+b.height/2);await page.mouse.down();await page.mouse.move(b.x+b.width/2+dx,b.y+b.height/2+dy,{steps:12});await page.mouse.up()} await page.goto(base+'/infinite_canvas'); await page.getByRole('button',{name:'从示例开始 ↗',exact:true}).click(); await page.getByRole('button',{name:'适应画布',exact:true}).click(); await content('brief').click({position:{x:20,y:20}}); assert.equal(await node('brief').getAttribute('data-selected'),'true');assert.equal(await node('brief').getAttribute('data-editing'),'false'); await page.waitForTimeout(700); await content('brief').click({position:{x:20,y:20}}); await node('brief').getByRole('textbox',{name:'01 / 创意简报 内容'}).fill('选择后再次点击进入编辑'); await waitField('brief','text','选择后再次点击进入编辑'); await node('brief').getByRole('button',{name:'加粗',exact:true}).click(); assert.equal((await snapshot()).cards.find(n=>n.id==='brief').style.bold,true); await node('brief').getByRole('button',{name:'斜体',exact:true}).click(); await node('brief').getByLabel('文字颜色',{exact:true}).fill('#78905e'); await node('brief').getByRole('combobox',{name:'文字样式',exact:true}).selectOption('24'); await page.waitForFunction(()=>{const n=JSON.parse(localStorage.getItem('studio.infinite-canvas.v1')).cards.find(n=>n.id==='brief');return n.style.bold&&n.style.italic&&n.style.color==='#78905e'&&n.style.size===24}); assert.deepEqual(await node('brief').getByRole('textbox',{name:'01 / 创意简报 内容'}).evaluate(el=>{const s=getComputedStyle(el);return {size:s.fontSize,weight:s.fontWeight,style:s.fontStyle,color:s.color}}),{size:'24px',weight:'700',style:'italic',color:'rgb(120, 144, 94)'}); const compact=(await snapshot()).cards.find(n=>n.id==='brief'); await node('brief').getByRole('button',{name:'展开文本节点',exact:true}).click(); const expanded=(await snapshot()).cards.find(n=>n.id==='brief');assert.ok(expanded.width>(compact.width||320)&&expanded.height>(compact.height||350)); await capture('canvas-text-format-desktop'); await page.getByRole('button',{name:'撤销',exact:true}).click(); assert.equal((await snapshot()).cards.find(n=>n.id==='brief').width,compact.width);assert.equal((await snapshot()).cards.find(n=>n.id==='brief').height,compact.height); await page.keyboard.press('Escape'); const before=(await snapshot()).cards.find(n=>n.id==='brief'); const z=await page.locator('[class*=world]').evaluate(el=>new DOMMatrix(getComputedStyle(el).transform).a); await drag(content('brief'),45,25); const after=(await snapshot()).cards.find(n=>n.id==='brief');close(after.x-before.x,45/z);close(after.y-before.y,25/z); assert.equal(await node('brief').getAttribute('data-editing'),'false'); await content('reference').click();assert.equal(await composer.count(),0); await page.waitForTimeout(700);await content('reference').click();await composer.waitFor();assert.equal(choosers,0,'Entering image editing must not force a file picker'); const panelBefore=await composer.boundingBox(),imageBefore=await node('reference').boundingBox(); await capture('canvas-composer-desktop'); await page.getByRole('button',{name:'缩小',exact:true}).click(); const panelAfter=await composer.boundingBox(),imageAfter=await node('reference').boundingBox(); close(panelBefore.width,panelAfter.width);close(panelBefore.height,panelAfter.height);assert.ok(imageAfter.width!!JSON.parse(localStorage.getItem('studio.infinite-canvas.v1')).cards.find(n=>n.id==='reference').image); await node('reference').getByRole('button',{name:'图片高清 ↗',exact:true}).click(); const upscale=(await snapshot()).cards.find(n=>n.operation==='upscale');assert.ok(upscale); await unobstructed(upscale.id); const download=page.waitForEvent('download');await composer.getByRole('button',{name:'导出任务',exact:true}).click(); const task=JSON.parse(await fs.readFile(await (await download).path(),'utf8')); assert.equal(task.target.id,upscale.id);assert.equal(task.references[0].id,'reference');assert.ok(task.references[0].image); await page.keyboard.press('Escape');await page.getByRole('button',{name:'适应画布',exact:true}).click(); await content('brief').click({position:{x:20,y:20}}); await node('brief').getByRole('button',{name:'引用 01 / 创意简报 生成',exact:true}).click(); await page.getByRole('dialog',{name:'画布操作'}).getByRole('button',{name:'脚本',exact:true}).click(); const script=(await snapshot()).cards.find(n=>n.kind==='script');assert.ok(script); await unobstructed(script.id); await composer.getByRole('textbox',{name:'创作要求',exact:true}).fill('基于简报写六个分镜'); await waitField(script.id,'instruction','基于简报写六个分镜');assert.equal((await snapshot()).cards.find(n=>n.id===script.id).text,''); await composer.getByRole('button',{name:'画布参考',exact:true}).click(); await composer.getByRole('group',{name:'选择引用节点'}).getByRole('button',{name:'02 / 视觉参考',exact:true}).click(); assert.ok((await snapshot()).links.some(([a,b])=>a==='reference'&&b===script.id)); await composer.getByRole('button',{name:'移除引用 01 / 创意简报',exact:true}).click(); assert.ok(!(await snapshot()).links.some(([a,b])=>a==='brief'&&b===script.id)); await page.keyboard.press('Escape'); await page.getByRole('button',{name:'适应画布',exact:true}).click(); await content('reference').click();await node('reference').getByRole('button',{name:'02 / 视觉参考 操作',exact:true}).click(); await page.getByRole('button',{name:'删除节点',exact:true}).click(); assert.ok((await snapshot()).cards.some(n=>n.id===script.id));assert.ok((await snapshot()).links.every(l=>!l.includes('reference'))); await page.getByRole('button',{name:'撤销',exact:true}).click();assert.ok((await snapshot()).links.some(([a,b])=>a==='reference'&&b===script.id)); await page.keyboard.press('Escape'); await content('brief').click({position:{x:20,y:20}}); const priorWidth=(await snapshot()).cards.find(n=>n.id==='brief').width; await drag(node('brief').getByRole('button',{name:'调整 01 / 创意简报 尺寸',exact:true}),50,30); assert.ok((await snapshot()).cards.find(n=>n.id==='brief').width>(priorWidth||320)); await page.getByRole('button',{name:'撤销',exact:true}).click();assert.equal((await snapshot()).cards.find(n=>n.id==='brief').width,priorWidth); await page.getByRole('button',{name:'重做',exact:true}).click(); await page.getByRole('button',{name:'移动画布',exact:true}).click(); await page.mouse.move(15,300);await page.mouse.down();await page.mouse.move(75,340,{steps:8});await page.mouse.up(); assert.equal(await page.evaluate(()=>{const path=[...document.querySelectorAll('svg path')].find(p=>p.getAttribute('d')?.startsWith('M '));const d=path.getAttribute('d').match(/-?\d+(?:\.\d+)?/g).map(Number),point=new DOMPoint(d[0],d[1]).matrixTransform(path.getScreenCTM()),card=document.querySelector('[data-node-id="brief"]').getBoundingClientRect();return Math.abs(point.x-card.right)<3&&Math.abs(point.y-(card.top+card.height/2))<3}),true); const worldAt=()=>page.locator('[class*=world]').evaluate(el=>{const m=new DOMMatrix(getComputedStyle(el).transform);return {x:(1450-m.e)/m.a,y:(150-m.f)/m.d,z:m.a}}); const anchorBefore=await worldAt();await page.mouse.move(1450,150);await page.keyboard.down('Control');await page.mouse.wheel(0,-80);await page.keyboard.up('Control'); await page.waitForFunction(z=>new DOMMatrix(getComputedStyle(document.querySelector('[class*=world]')).transform).a!==z,anchorBefore.z); const anchorAfter=await worldAt();close(anchorBefore.x,anchorAfter.x);close(anchorBefore.y,anchorAfter.y); const saved=await snapshot();await page.reload();await page.waitForFunction(()=>!!localStorage.getItem('studio.infinite-canvas.v1'));assert.deepEqual(await snapshot(),saved); await page.getByRole('button',{name:'切换明暗主题',exact:true}).click();await page.reload();await page.waitForFunction(()=>document.querySelector('main').dataset.theme==='light'); if(process.env.SCREENSHOT_DIR){await fs.mkdir(process.env.SCREENSHOT_DIR,{recursive:true});await page.screenshot({path:process.env.SCREENSHOT_DIR+'/canvas-nodes-desktop.png'});} await page.setViewportSize({width:390,height:844});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)); const viewport=await page.locator('[class*=viewport]').boundingBox();assert.equal(viewport.width,390);assert.equal(viewport.height,844); if(process.env.SCREENSHOT_DIR)await page.screenshot({path:process.env.SCREENSHOT_DIR+'/canvas-nodes-mobile.png'}); assert.deepEqual(errors,[]);console.log('Passed: selected-node re-click editing, drag, formatting, fixed composer, upload, references, task export, deletion/undo, reload and mobile bounds'); }catch(error){if(process.env.SCREENSHOT_DIR&&page){await fs.mkdir(process.env.SCREENSHOT_DIR,{recursive:true});await page.screenshot({path:process.env.SCREENSHOT_DIR+'/canvas-failure.png'}).catch(()=>{})}throw error}finally{await browser.close()} })().catch(e=>{console.error(e);process.exitCode=1});