const {chromium}=require('playwright-core'),assert=require('node:assert/strict'),fs=require('node:fs'); const root='D:/web/dj/integration-work/',base='http://127.0.0.1:18885'; const fixture=JSON.parse(fs.readFileSync(root+'hd-test-panels/shot-ui-fixture.json')); (async()=>{ const browser=await chromium.launch({channel:'chrome',headless:true}); try{ const page=await browser.newPage({viewport:{width:1700,height:1150}}),errors=[]; page.on('pageerror',e=>errors.push(e.message)); let submitted=null,replacement=null; await page.route('**/api/v1/tasks**',async route=>{ const req=route.request(),u=new URL(req.url()); if(req.method()==='POST'&&u.pathname==='/api/v1/tasks'){ submitted=req.postDataJSON(); replacement={id:fixture.ids[1]+100,type:'video',storyboard_id:1,drama_id:1,provider:'minimax',model:submitted.model,config_id:submitted.config_id, task_id:'ui-new-job',status:'processing',stage:'polling',created_at:new Date().toISOString(),params:JSON.stringify({gpu:submitted.gpu}),routing:{gpu:'gpu1',port:8189,legacy:false}}; return route.fulfill({status:201,contentType:'application/json',body:JSON.stringify({data:replacement})}); } if(req.method()==='GET'&&replacement){ if(u.pathname==='/api/v1/tasks/'+replacement.id)return route.fulfill({json:{data:replacement}}); if(u.pathname==='/api/v1/tasks'){ const response=await route.fetch(),json=await response.json(); json.data=json.data.map(t=>t.id===fixture.ids[0]?{...t,status:'failed',error_code:'user_cancelled'}:t); if(!u.searchParams.has('type')||u.searchParams.get('type')==='video')json.data.push(replacement); return route.fulfill({response,json}); } } return route.continue(); }); await page.route('**/api/v1/episodes/1/generation-tasks',async route=>{ const response=await route.fetch(),json=await response.json(); if(replacement){json.data.tasks=json.data.tasks.map(t=>t.id===fixture.ids[0]?{...t,status:'failed',error_code:'user_cancelled'}:t);json.data.tasks.push(replacement)} return route.fulfill({response,json}); }); await page.goto(base+'/drama/1/episode/1',{waitUntil:'networkidle'}); await page.getByRole('navigation').getByRole('button',{name:/^视频制作/}).click(); const rows=page.locator('.video-task-row'),gpu=page.getByLabel('本次生成显卡',{exact:true}); const actual=page.getByTestId('shot-actual-routing'); await actual.filter({hasText:'GPU2 · ComfyUI 8188'}).waitFor(); const top=page.locator('.studio-topbar .model-select').filter({has:page.locator('.model-select-label',{hasText:'视频默认'})}); const beforeDefault=await top.innerText(); const shotModel=page.locator('.video-inspector-settings .model-select'); await shotModel.locator('button').click(); await page.getByText('GPU1 · ComfyUI 8189 · MiniMax-H3',{exact:true}).last().click(); await gpu.selectOption('gpu1'); assert.equal(await top.innerText(),beforeDefault); await rows.nth(1).click(); assert.equal(await gpu.inputValue(),'gpu3'); assert.match(await actual.innerText(),/GPU3 · ComfyUI 8191/); await rows.first().click();assert.equal(await gpu.inputValue(),'gpu1'); // Before submission, the current-job badge still reports the old confirmed worker. assert.match(await actual.innerText(),/GPU2 · ComfyUI 8188/); await page.getByRole('button',{name:'取消并换卡重新生成',exact:true}).click(); await actual.filter({hasText:'GPU1 · ComfyUI 8189'}).waitFor(); assert.equal(submitted.storyboard_id,1);assert.equal(submitted.replace_task_id,fixture.ids[0]);assert.equal(submitted.gpu,'gpu1');assert.equal(submitted.config_id,11); assert.match(await rows.nth(1).locator('.video-task-routing').innerText(),/GPU3 :8191/); assert.match(await rows.first().locator('.video-task-state').innerText(),/生成中/); await page.screenshot({path:root+'shot-specific-routing-ui.png'}); await page.reload({waitUntil:'networkidle'}); await page.getByRole('navigation').getByRole('button',{name:/^视频制作/}).click(); await actual.filter({hasText:'GPU1 · ComfyUI 8189'}).waitFor(); await page.getByRole('navigation').getByRole('button',{name:/^高清制作/}).click(); const hdGpu=page.getByLabel('高清生成显卡',{exact:true});await hdGpu.waitFor(); await hdGpu.selectOption('gpu1');await page.getByLabel('目标分辨率').selectOption('4K'); const shots=page.locator('.hd-shots button');await shots.nth(1).click(); assert.notEqual(await hdGpu.inputValue(),'gpu1');assert.equal(await page.getByLabel('目标分辨率').inputValue(),'2K'); await hdGpu.selectOption('gpu3');await shots.first().click(); assert.equal(await hdGpu.inputValue(),'gpu1');assert.equal(await page.getByLabel('目标分辨率').inputValue(),'4K'); await page.locator('.hd-studio').getByRole('button',{name:'刷新',exact:true}).click();assert.equal(await hdGpu.inputValue(),'gpu1'); assert.deepEqual(errors,[]); fs.writeFileSync(root+'shot-routing-ui-result.json',JSON.stringify({passed:true,checks:['per-shot model and GPU choices','batch default unchanged','exact selected task replacement','confirmed routing appears only after submission','other shot unchanged','routing survives reload','HD settings scoped per shot'],errors},null,2)); console.log('PASS: individual shot model/GPU selection and actual worker badges, persistent task routing, independent HD choices'); }finally{await browser.close()} })().catch(e=>{console.error(e.stack);process.exit(1)});