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;});