| 1 | import { createServer } from 'vite'; |
| 2 | import { chromium } from 'playwright'; |
| 3 | import path from 'node:path'; |
| 4 | import { fileURLToPath } from 'node:url'; |
| 5 | const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); |
| 6 | const server=await createServer({root,server:{host:'127.0.0.1',port:0},logLevel:'error'}); |
| 7 | await server.listen(); |
| 8 | let browser; |
| 9 | try { |
| 10 | browser=await chromium.launch({headless:true,channel:'chrome'}); |
| 11 | const page=await browser.newPage({viewport:{width:1280,height:850}}); |
| 12 | const errors=[];page.on('pageerror',e=>errors.push(String(e))); |
| 13 | const url=server.resolvedUrls.local[0]; |
| 14 | page.setDefaultTimeout(15000); |
| 15 | await page.goto(url+'?mock=fresh',{waitUntil:'domcontentloaded'}); |
| 16 | const later=page.getByRole('button',{name:/稍后设置|Set up later/}); |
| 17 | try { await later.waitFor({state:'visible',timeout:3000}); await later.click(); } catch {} |
| 18 | const composer=page.locator('#composer-input');await composer.waitFor({state:'visible',timeout:60000}); |
| 19 | const send=async text=>{await composer.fill(text);await page.locator('.composer__btn--send').click();}; |
| 20 | await send('/mock-protocol-recovery'); console.log('sent mock failure'); |
| 21 | const recover=page.getByRole('button',{name:/从有效历史恢复|Recover from valid history/}); |
| 22 | await recover.waitFor({state:'visible'}); |
| 23 | if(await recover.count()!==1)throw Error('duplicate recovery cards'); |
| 24 | await recover.click(); |
| 25 | const stop=page.locator('.composer__btn--stop');await stop.waitFor({state:'visible'});await stop.click(); |
| 26 | await stop.waitFor({state:'hidden'}); |
| 27 | await page.waitForTimeout(1700); |
| 28 | if(await page.locator('.tool__search-summary').count())throw Error('late response after cancellation committed'); |
| 29 | await send('/mock-protocol-recovery');await recover.waitFor({state:'visible'});await recover.click(); |
| 30 | const note=page.getByText(/搜索已完成,供应商未提供可用的结构化来源|Search completed; the provider/).first(); |
| 31 | await note.waitFor({state:'visible',timeout:15000}); |
| 32 | const summary=page.getByText('搜索摘要仍然可读。'); |
| 33 | const header=page.locator('.tool__head').last(); |
| 34 | if(await header.count()===0)await page.locator('.turn-collapse__label').last().click(); |
| 35 | if(await header.getAttribute('aria-expanded')==='false')await header.click(); |
| 36 | await summary.waitFor({state:'visible'}); |
| 37 | await summary.scrollIntoViewIfNeeded(); |
| 38 | await page.waitForTimeout(300); |
| 39 | const summaryBox=await summary.boundingBox(); |
| 40 | const composerBox=await page.locator('.composer').boundingBox(); |
| 41 | if(!summaryBox || !composerBox || summaryBox.y+summaryBox.height>composerBox.y || summaryBox.y<0)throw Error('search summary is outside the readable transcript viewport'); |
| 42 | await page.screenshot({path:'/tmp/reasonix-protocol-recovery-browser.png',fullPage:true}); |
| 43 | if(errors.length)throw Error(errors.join('\n')); |
| 44 | console.log('PASS: one recovery button, click, stop, no late search, source-free successful search and summary; no page errors'); |
| 45 | } catch(error) { console.error(error); if(browser){const p=browser.contexts()[0]?.pages()[0]; if(p){await p.screenshot({path:'/tmp/reasonix-protocol-browser-failure.png'});console.error((await p.locator('body').innerText()).slice(-4500));}} throw error; } finally {await browser?.close();await server.close();} |
| 46 |