import { chromium } from '../tools/playwright/node_modules/playwright/index.mjs'; import assert from 'node:assert/strict'; const browser=await chromium.launch({headless:true}); try { for(const theme of ['light','dark']) { const page=await browser.newPage({viewport:{width:1440,height:900},serviceWorkers:'block'}); await page.addInitScript(theme=>{localStorage.setItem('lazyboy.theme',theme);localStorage.setItem('lazyboy.locale','zh-Hant')},theme); let task='old-task',state='running',stops=0; const activity=()=>({state,running:state==='running',queued:false,observed_at_ms:Date.now(),active_task_ids:state==='running'?[task]:[]}); const agent=()=>({id:'fixture',name:'小麻糬',running:true,activity:activity(),avatar_color:'#eec78c'}); const transcript=[{role:'user',content:'幫我整理今天的工作,還有接下來的步驟。',at:'2026-09-16T08:00:00Z'},{role:'assistant',content:'可以,我會先整理重點,再列出待辦事項。\n\n### 今天的進度\n- 電腦畫面恢復完整\n- 新的透明背景圖示\n- 深淺主題都能清楚閱讀\n\n使用 `npm run build` 驗證。\n\n```js\nconst ready = true;\n```',at:'2026-09-16T08:00:01Z'}]; await page.route('**/api/**',async route=>{ const path=new URL(route.request().url()).pathname; if(path.endsWith('/stop')) {stops++;await new Promise(r=>setTimeout(r,600));return route.fulfill({json:{ok:true}})} if(path.endsWith('/activity'))return route.fulfill({json:activity()}); if(path.endsWith('/events'))return route.fulfill({contentType:'text/event-stream',body:'data: {"events":[]}\n\n'}); if(path.endsWith('/computer'))return route.fulfill({json:{ready:true,state:'ready',viewer_url:'/fixture-screen'}}); if(path==='/api/agents')return route.fulfill({json:{agents:[agent()]}}); if(path==='/api/agents/fixture')return route.fulfill({json:{...agent(),transcript}}); return route.fulfill({json:{}}); }); await page.route('**/fixture-screen?*',r=>r.fulfill({contentType:'text/html',body:'Desktop preview'})); await page.goto('http://127.0.0.1:5173'); await page.locator('.stop-send').waitFor(); assert.ok(await page.locator('.avatar.thinking').count()>0); await page.locator('.stop-send').click(); assert.equal(await page.locator('.avatar.thinking').count(),0,'stop must immediately remove all thinking rings'); await page.waitForTimeout(4500); // stale activity + roster poll must not revive stopped work assert.equal(stops,1);assert.equal(await page.locator('.avatar.thinking').count(),0); await page.screenshot({path:`.run/appearance-${theme}.png`}); const colors=await page.locator('.message.assistant .message-body').first().evaluate(el=>({bg:getComputedStyle(el).backgroundColor,fg:getComputedStyle(el).color})); if(theme==='light'){assert.equal(colors.bg,'rgb(240, 241, 243)');assert.equal(colors.fg,'rgb(20, 20, 20)')} task='new-task'; await page.locator('.stop-send').waitFor({timeout:6000}); assert.ok(await page.locator('.avatar.thinking').count()>0,'new work must animate normally'); state='idle'; await page.locator('.stop-send').waitFor({state:'hidden',timeout:6000}); assert.equal(await page.locator('.bot-row.selected .avatar.thinking').count(),0,'idle activity wins over stale roster'); console.log(`PASS ${theme}: immediate stop, stale activity suppression, new task and idle avatar`); await page.close(); } }finally{await browser.close()}