'use strict'; // Isolated UI regression: all requests use fixture data, never the user's queue. const {chromium}=require('playwright'); const fs=require('node:fs'); const path=require('node:path'); const assert=require('node:assert/strict'); const ROOT=path.resolve(__dirname,'..'); (async()=>{ const browser=await chromium.launch({headless:true,executablePath:process.env.CHROME_PATH||'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'}); try{ const page=await browser.newPage({viewport:{width:1440,height:1000}}),errors=[],requests=[]; page.on('pageerror',error=>errors.push(error.message)); const data={run:{running:false,active:'active',config:{},reason:'已暂停',batch:null},clients:[], jobs:['a','b','active'].map(id=>({id,title:'测试文章 '+id,original_title:'测试文章 '+id,image_count:0,source:'测试来源',state:id==='active'?'awaiting_user_review':'queued'})), download:{status:'idle',saved:0,target:0,skipped:0,failed:0,logs:[]}}; let failRemoval=false; await page.route('**/*',async route=>{ const url=new URL(route.request().url()); if(url.hostname!=='queue.test')return route.abort(); if(url.pathname==='/api/session')return route.fulfill({json:{token:'fixture'}}); if(url.pathname==='/api/state')return route.fulfill({json:data}); if(url.pathname==='/api/remove'){ const body=route.request().postDataJSON();requests.push(body); if(failRemoval)return route.fulfill({status:400,json:{error:'模拟移除失败'}}); data.jobs=data.jobs.filter(job=>!body.ids.includes(job.id)); return route.fulfill({json:{ok:true,removed:body.ids.length}}); } const files={'/':'index.html','/app.js':'app.js','/style.css':'style.css'}; if(files[url.pathname])return route.fulfill({path:path.join(ROOT,'static',files[url.pathname])}); return route.abort(); }); await page.goto('http://queue.test/'); await page.waitForFunction(()=>document.querySelectorAll('.job-select').length===3); const box=id=>page.locator(`.job-select[value="${id}"]`); assert.equal(await page.locator('#remove-selected').isDisabled(),true); assert.equal(await box('active').isDisabled(),true); await box('a').check(); assert.equal(await page.locator('#select-all').evaluate(el=>el.indeterminate),true); await page.evaluate(()=>refresh()); assert.equal(await box('a').isChecked(),true,'polling preserves selection'); page.once('dialog',dialog=>dialog.dismiss()); await page.locator('#remove-selected').click(); assert.equal(requests.length,0,'cancel does not delete'); failRemoval=true; page.once('dialog',dialog=>dialog.accept()); await page.locator('#remove-selected').click(); await page.getByText('模拟移除失败',{exact:true}).waitFor(); assert.equal(await box('a').isChecked(),true,'failure preserves selection'); failRemoval=false; page.once('dialog',dialog=>dialog.accept()); await page.locator('#remove-selected').click(); await page.waitForFunction(()=>document.querySelectorAll('.job-select').length===2); assert.deepEqual(requests.at(-1),{ids:['a']}); assert.equal(await box('b').count(),1,'unselected article remains'); await page.locator('#select-all').check(); assert.equal(await box('b').isChecked(),true); assert.equal(await box('active').isChecked(),false); data.run.running=true;await page.evaluate(()=>refresh()); assert.equal(await page.locator('#remove-selected').isDisabled(),true,'running flow blocks removal'); data.run.running=false;data.run.active=null;await page.evaluate(()=>refresh()); await page.locator('#select-all').check(); await page.setViewportSize({width:390,height:844}); assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false,'mobile has no horizontal overflow'); page.once('dialog',dialog=>dialog.accept()); await page.locator('#remove-selected').click(); await page.locator('#empty').waitFor({state:'visible'}); assert.equal(await page.locator('#total').textContent(),'0'); assert.equal(await page.locator('#select-all').isDisabled(),true); assert.equal(await page.locator('#remove-selected').isDisabled(),true); assert.deepEqual(errors,[]); console.log('PASS queue UI: selection, polling, cancellation, failure, selected deletion, protected task, running state, select all, empty state, mobile layout'); }finally{await browser.close();} })().catch(error=>{console.error(error);process.exitCode=1;});