const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const {chromium} = require('playwright'); const root = path.resolve(__dirname, '../html'); (async () => { const browser = await chromium.launch({headless:true,channel:'msedge'}); try { const page = await browser.newPage(); await page.route('**/*', route => { const url = new URL(route.request().url()); if (url.pathname === '/api/data-quality') return route.fulfill({json:{data_version:'v',total:0,datasets:{}}}); const file = path.join(root, url.pathname === '/' ? 'index.html' : url.pathname); if (!file.startsWith(root) || !fs.existsSync(file)) return route.fulfill({status:404,body:''}); return route.fulfill({body:fs.readFileSync(file),contentType:file.endsWith('.js') ? 'application/javascript' : file.endsWith('.css') ? 'text/css' : 'text/html'}); }); await page.goto('http://qa.test/index.html'); await page.evaluate(() => { appendMessage('assistant', '# 标题\n\n## 统计\n\n- **重点**\n- 第二项\n\n| 项目 | 人数 |\n| --- | --- |\n| 示例 | 2 |\n\n```text\n# 代码原样\n```'); appendMessage('user', '# 用户输入'); }); assert.equal(await page.locator('.markdown-body h1').innerText(),'标题'); assert.equal(await page.locator('.markdown-body h2').innerText(),'统计'); assert.equal(await page.locator('.markdown-body li').count(),2); assert.equal(await page.locator('.markdown-body table td').count(),2); assert.equal(await page.locator('pre code').innerText(),'# 代码原样\n'); assert.equal(await page.locator('.message.user').innerText(),'# 用户输入'); await page.evaluate(() => appendMessage('assistant','[危险](javascript:alert(1))\n\n[来源](https://example.com)')); assert.equal(await page.locator('.markdown-body script, .markdown-body img, .markdown-body [onerror], .markdown-body a[href^="javascript:"]').count(),0); assert.equal(await page.evaluate(() => window.bad),undefined); // Send encoded SSE bytes in small chunks, including splits inside Chinese characters. await page.evaluate(async () => { const events = [['answer_chunk',{text:'# 分'}],['answer_chunk',{text:'块标题\n\n**粗'}],['answer_chunk',{text:'体**'}],['done',{answer:'# 最终标题\n\n**完整答案**'}]]; const bytes = new TextEncoder().encode(events.map(([event,data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`).join('')); const original = window.fetch; window.fetch = async () => new Response(new ReadableStream({start(c) {for(let i=0;i { const original=window.fetch; window.fetch=async () => new Response(JSON.stringify({status:'ok',answer:'### 确认后的回答\n\n1. 第一项'})); try { await resumeThread('确认'); } finally { window.fetch=original; } }); assert.equal(await page.locator('.markdown-body h3').innerText(),'确认后的回答'); await page.evaluate(() => showConfirm('请选择', ['1. 水电工', '2. 维修电工', '都不是'], 'clarify_value')); await page.locator('#confirmOptions .option-chip').nth(0).click(); await page.locator('#confirmOptions .option-chip').nth(1).click(); assert.equal(await page.locator('#confirmOptions .option-chip.selected').count(),2); assert.equal(await page.locator('#confirmReply').inputValue(),'1. 水电工,2. 维修电工'); await page.locator('#confirmOptions .option-chip').nth(2).click(); assert.equal(await page.locator('#confirmOptions .option-chip.selected').count(),1); assert.equal(await page.locator('#confirmReply').inputValue(),'都不是'); await page.evaluate(() => showConfirm('普通确认', ['确认', '修改'], 'confirm_plan')); await page.locator('#confirmOptions .option-chip').nth(0).click(); await page.locator('#confirmOptions .option-chip').nth(1).click(); assert.equal(await page.locator('#confirmReply').inputValue(),'修改'); await page.setViewportSize({width:390,height:844}); assert.ok(await page.locator('.markdown-body h1').first().isVisible()); console.log('PASS: headings, lists, table, code, user text, XSS filtering, SSE UTF-8 chunks/final replacement, resume, candidate multi-select and mobile'); } finally { await browser.close(); } })().catch(e => {console.error(e);process.exitCode=1;});