const {chromium}=require('playwright-core'),assert=require('node:assert/strict'); (async()=>{ const browser=await chromium.launch({channel:'chrome',headless:true}); try { const page=await browser.newPage({viewport:{width:1500,height:1100}}),errors=[],calls=[],states={}; page.on('pageerror',e=>errors.push(e.message)); await page.addInitScript(()=>localStorage.setItem('huobao:tours',JSON.stringify(['episode']))); await page.route('**/api/**',async route=>{ const req=route.request(),url=req.url(),match=url.match(/\/tasks\/(\d+)\/cancel$/); if(req.method()==='POST'&&match){ const id=+match[1];assert.ok([237,239].includes(id));calls.push(id); states[id]=id===239&&!states[id]?'pending':'cancelled'; return route.fulfill({status:200,json:{code:200,data:{outcome:states[id]}}}); } if(!['GET','HEAD'].includes(req.method()))throw Error('Unexpected mutation: '+req.method()+' '+url); if(url.includes('/generation-tasks')){ const response=await route.fetch(),body=await response.json(); for(const task of body.data.tasks){ if(![237,239].includes(task.id))continue; task.status=states[task.id]==='cancelled'?'failed':'processing'; task.error_code=states[task.id]==='cancelled'?'user_cancelled':null; task.error_msg=states[task.id]==='cancelled'?'任务已取消,历史记录和已有素材保留':null; task.stage=states[task.id]==='pending'?'cancelling':task.id===239?'queued':'running'; task.can_cancel=task.status==='processing'; } return route.fulfill({response,json:body}); } return route.continue(); }); await page.goto('http://127.0.0.1:18938/drama/6/episode/1',{waitUntil:'networkidle'}); await page.getByRole('button',{name:/^任务/}).click(); const drawer=page.locator('.task-drawer-overlay');await drawer.waitFor(); const row=id=>drawer.locator('.gen-task-row').filter({has:page.locator('.video-task-meta-line').filter({hasText:'#'+id})}); assert.equal(await row(237).getByRole('button',{name:'取消任务',exact:true}).count(),1); await row(239).getByRole('button',{name:'取消任务',exact:true}).click(); await row(239).getByRole('button',{name:'重试取消任务',exact:true}).waitFor(); assert.match(await row(239).innerText(),/正在等待显卡确认/); assert.equal(await row(237).getByRole('button',{name:'取消任务',exact:true}).count(),1); await row(239).getByRole('button',{name:'重试取消任务',exact:true}).click(); await row(239).locator('.video-task-status.is-cancelled').waitFor(); assert.match(await row(239).innerText(),/已取消/); assert.equal(await row(239).getByRole('button',{name:/取消任务/}).count(),0); await row(237).getByRole('button',{name:'取消任务',exact:true}).click(); await row(237).locator('.video-task-status.is-cancelled').waitFor(); assert.deepEqual(calls,[239,239,237]);assert.deepEqual(errors,[]); await page.screenshot({path:'D:/web/dj/integration-work/cancel-ui-fixture.png'}); console.log('PASS: per-image/video cancel buttons; pending confirmation/retry; cancelled styling/counts; no real cancellations'); }finally{await browser.close()} })().catch(e=>{console.error(e.stack);process.exit(1)});