import { visitBrowserDocument } from "./brand-navigation.mjs"; import assert from 'node:assert/strict'; import {randomUUID} from 'node:crypto'; import {chromium,type Request} from 'playwright-core'; import {db} from '../src/lib/db'; import {hashPassword,ACCOUNT_COOKIE} from '../src/lib/account-crypto'; export async function verifyCanvasAgentUI(base:string){ assert.equal(process.env.PROMPT_INTEGRATION_TEST,'1'); const owner=randomUUID(),email=owner+'@example.com',password='Canvas integration example1!'; await db().query("INSERT INTO account_users(id,email,password_hash,verified_at) VALUES($1,$2,$3,now())",[owner,email,await hashPassword(password)]); const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']}); const context=await browser.newContext({viewport:{width:1440,height:1000}}),errors:string[]=[]; const page=await context.newPage(),canvasRequests:string[]=[],pending=new Set(),network:string[]=[]; const started=Date.now(),navigation:{ms:number;event:string;path:string}[]=[]; const mark=(event:string)=>{navigation.push({ms:Date.now()-started,event,path:new URL(page.url()).pathname});if(navigation.length>60)navigation.shift()}; page.on('framenavigated',frame=>{if(frame===page.mainFrame())mark('framenavigated')}); page.on('domcontentloaded',()=>mark('domcontentloaded'));page.on('load',()=>mark('load')); const requestName=(request:{method:()=>string;url:()=>string})=>request.method()+' '+new URL(request.url()).pathname; page.on('request',request=>pending.add(request)); page.on('requestfinished',request=>pending.delete(request)); page.on('requestfailed',request=>{pending.delete(request);network.push(requestName(request)+': '+request.failure()?.errorText)}); page.on('response',response=>{const url=new URL(response.url());if(url.pathname.startsWith('/api/canvases'))canvasRequests.push(`${response.request().method()} ${url.pathname} ${response.status()}`)}); try{ page.setDefaultTimeout(60000);page.setDefaultNavigationTimeout(60000);page.on('pageerror',e=>errors.push(e.message)); mark('open landing');await visitBrowserDocument(page,base+'/'); await page.getByRole('heading',{name:/想法不该停在对话里/,level:1}).waitFor(); const accountSession=page.waitForResponse(response=>new URL(response.url()).pathname==='/api/account/session'&&response.request().method()==='GET'); const [,sessionResponse]=await Promise.all([page.getByRole('link',{name:'开始创作',exact:true}).click(),accountSession]); assert.equal(sessionResponse.status(),200);assert.equal((await sessionResponse.json()).user,null); await page.getByRole('heading',{name:'欢迎回来',exact:true,level:2}).waitFor(); await page.locator('button[data-theme-ready="true"]').first().waitFor(); mark('login form ready'); await page.getByLabel('邮箱地址',{exact:true}).fill(email); await page.getByLabel('密码',{exact:true}).fill(password); mark('before login click');await page.locator('form').getByRole('button',{name:'登录',exact:true}).click(); mark('after login click'); await page.waitForURL(url=>url.pathname==='/workspace',{waitUntil:'commit'}); mark('workspace URL accepted'); await page.getByRole('heading',{name:'今天,让哪个想法成形?',exact:true}).waitFor(); await page.locator('button[data-theme-ready="true"]').first().waitFor(); mark('workspace rendered'); const session=(await context.cookies()).find(c=>c.name===ACCOUNT_COOKIE);assert.ok(session?.httpOnly,'The actual login response establishes an HTTP-only account session'); const ownerHeaders={cookie:ACCOUNT_COOKIE+'='+session.value}; const canvasLink=page.getByRole('link',{name:'无限画布',exact:true}); assert.equal(await canvasLink.getAttribute('href'),'/infinite_canvas'); await visitBrowserDocument(page,base+'/infinite_canvas'); await page.getByRole('link',{name:email,exact:true}).waitFor({state:'attached'}); await page.getByRole('button',{name:'从示例开始 ↗',exact:true}).click(); const shot=page.locator('[data-node-id="shot"] [data-node-content]'); await shot.click();await shot.click(); const composer=page.getByRole('region',{name:'节点编辑器',exact:true}); await composer.getByRole('textbox',{name:'创作要求',exact:true}).fill('根据参考资料写六个分镜'); await composer.getByRole('button',{name:'交给 Agent',exact:true}).click(); const panel=page.getByRole('region',{name:'Agent 连接',exact:true}); await panel.getByText('任务已创建,等待你的 coding agent 领取',{exact:true}).waitFor(); const list=await fetch(base+'/api/canvases',{headers:ownerHeaders}).then(r=>r.json());assert.equal(list.canvases.length,1); const root='/api/canvases/'+list.canvases[0].id; const state=await fetch(base+root,{headers:ownerHeaders}).then(r=>r.json());assert.equal(state.tasks.length,1);assert.equal(state.tasks[0].status,'queued'); await panel.getByRole('button',{name:'生成连接凭据',exact:true}).click(); const credentials=panel.getByRole('textbox',{name:'Agent 连接凭据',exact:true});await credentials.waitFor(); await panel.getByRole('button',{name:'复制给 coding agent',exact:true}).click(); await panel.getByRole('status').filter({hasText:/凭据已复制|已选中凭据/}).waitFor(); const first=JSON.parse(await credentials.inputValue());assert.equal(first.canvasUrl,base+root); const agentHeaders={'Content-Type':'application/json',authorization:first.authorization}; const task=root+'/tasks/'+state.tasks[0].id; const recipe=await fetch(base+task,{headers:agentHeaders}).then(r=>r.json());assert.equal(recipe.request.instruction,'根据参考资料写六个分镜');assert.equal(recipe.request.references[0].id,'reference'); const patch=async(body:unknown)=>fetch(base+task,{method:'PATCH',headers:agentHeaders,body:JSON.stringify(body)}); assert.equal((await patch({status:'running'})).status,200); await panel.getByText(/Agent 正在处理/).waitFor(); // This fixture verifies the external-client protocol and UI delivery, not model quality. let generated='第一镜:晨光穿过窗边的植物。'; assert.equal((await patch({status:'succeeded',result:{text:generated}})).status,200); await page.locator('[data-node-id="shot"] [data-node-content]').getByText(generated,{exact:true}).waitFor(); const updated=await fetch(base+root,{headers:ownerHeaders}).then(r=>r.json()); assert.equal(updated.document.cards.find((n:{id:string})=>n.id==='shot').instruction,'根据参考资料写六个分镜'); await panel.getByRole('button',{name:'关闭 Agent 连接',exact:true}).click(); await shot.click();await shot.click(); await composer.getByRole('textbox',{name:'创作要求',exact:true}).fill('再写一个新镜头'); const queued=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname===root+'/tasks'); await composer.getByRole('button',{name:'交给 Agent',exact:true}).click(); const queuedResponse=await queued;assert.equal(queuedResponse.status(),201);const secondTask=await queuedResponse.json(); await panel.getByRole('button',{name:'关闭 Agent 连接',exact:true}).click(); const brief=page.locator('[data-node-id="brief"] [data-node-content]');await brief.click();await brief.click(); const localText=page.getByRole('textbox',{name:'01 / 创意简报 内容',exact:true});await localText.fill('用户尚未同步的本机修改'); const nextTask=root+'/tasks/'+secondTask.id; const nextPatch=(body:unknown)=>fetch(base+nextTask,{method:'PATCH',headers:agentHeaders,body:JSON.stringify(body)}); assert.equal((await nextPatch({status:'running'})).status,200); generated='第二镜:城市在雨后醒来。'; assert.equal((await nextPatch({status:'succeeded',result:{text:generated}})).status,200); await page.getByRole('button',{name:'Agent · 在线画布',exact:true}).click(); await panel.getByRole('button',{name:'载入在线版本',exact:true}).waitFor(); assert.equal(await localText.inputValue(),'用户尚未同步的本机修改','Polling must not replace unsaved local work'); await panel.getByRole('button',{name:'载入在线版本',exact:true}).click(); await page.locator('[data-node-id="shot"] [data-node-content]').getByText(generated,{exact:true}).waitFor(); assert.equal(await page.locator('[data-node-id="brief"] [data-node-content]').textContent(),state.document.cards.find((n:{id:string})=>n.id==='brief').text); await panel.getByRole('button',{name:'生成连接凭据',exact:true}).click(); await page.waitForFunction(old=>{const field=document.querySelector('textarea[aria-label="Agent 连接凭据"]');return !!field&&JSON.parse(field.value).authorization!==old},first.authorization); const second=JSON.parse(await credentials.inputValue());assert.equal((await fetch(base+root,{headers:agentHeaders})).status,404); await panel.getByRole('button',{name:'撤销连接',exact:true}).click(); await panel.getByText('Agent 访问已撤销',{exact:true}).waitFor(); assert.equal((await fetch(base+root,{headers:{authorization:second.authorization}})).status,404); await page.setViewportSize({width:390,height:844}); const bounds=await panel.boundingBox();assert.ok(bounds&&bounds.x>=0&&bounds.x+bounds.width<=390,'Agent panel stays within the mobile viewport'); await visitBrowserDocument(page,page.url(),true); await page.getByRole('link',{name:email,exact:true,includeHidden:true}).waitFor({state:'attached'}); await page.getByRole('button',{name:'Agent',exact:true}).click(); const reopened=page.getByRole('region',{name:'Agent 连接',exact:true}); await reopened.getByRole('combobox',{name:'打开在线画布',exact:true}).selectOption(list.canvases[0].id); await reopened.getByRole('button',{name:'载入在线版本',exact:true}).click(); await page.locator('[data-node-id="shot"] [data-node-content]').getByText(generated,{exact:true}).waitFor(); assert.deepEqual(errors,[]); console.log('Canvas navigation timeline',JSON.stringify(navigation)); console.log('Canvas UI dispatched a real stored task, received an external-client fixture result, rotated/revoked access and restored its online document'); }catch(error){ const statuses=await page.getByRole('status').allTextContents().catch(()=>[]); const headings=await page.getByRole('heading').allTextContents().catch(()=>[]); const documentState=await page.evaluate(()=>({readyState:document.readyState,navigation:performance.getEntriesByType('navigation').map(entry=>{const timing=entry as PerformanceNavigationTiming;return {type:timing.type,domContentLoadedEventEnd:timing.domContentLoadedEventEnd,loadEventEnd:timing.loadEventEnd}})})).catch(()=>null); console.error('Canvas agent UI diagnostics',JSON.stringify({url:page.url(),browserConnected:browser.isConnected(),headings,statuses,documentState,navigation,pending:[...pending].map(requestName),network:network.slice(-20),canvasRequests:canvasRequests.slice(-20),errors})); throw error; }finally{await context.close();await browser.close()} }