test_answer_markdown_ui.cjs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. const assert = require('node:assert/strict');
  2. const fs = require('node:fs');
  3. const path = require('node:path');
  4. const {chromium} = require('playwright');
  5. const root = path.resolve(__dirname, '../html');
  6. (async () => {
  7. const browser = await chromium.launch({headless:true,channel:'msedge'});
  8. try {
  9. const page = await browser.newPage();
  10. await page.route('**/*', route => {
  11. const url = new URL(route.request().url());
  12. if (url.pathname === '/api/data-quality') return route.fulfill({json:{data_version:'v',total:0,datasets:{}}});
  13. const file = path.join(root, url.pathname === '/' ? 'index.html' : url.pathname);
  14. if (!file.startsWith(root) || !fs.existsSync(file)) return route.fulfill({status:404,body:''});
  15. return route.fulfill({body:fs.readFileSync(file),contentType:file.endsWith('.js') ? 'application/javascript' : file.endsWith('.css') ? 'text/css' : 'text/html'});
  16. });
  17. await page.goto('http://qa.test/index.html');
  18. await page.evaluate(() => {
  19. appendMessage('assistant', '# 标题\n\n## 统计\n\n- **重点**\n- 第二项\n\n| 项目 | 人数 |\n| --- | --- |\n| 示例 | 2 |\n\n```text\n# 代码原样\n```');
  20. appendMessage('user', '# 用户输入');
  21. });
  22. assert.equal(await page.locator('.markdown-body h1').innerText(),'标题');
  23. assert.equal(await page.locator('.markdown-body h2').innerText(),'统计');
  24. assert.equal(await page.locator('.markdown-body li').count(),2);
  25. assert.equal(await page.locator('.markdown-body table td').count(),2);
  26. assert.equal(await page.locator('pre code').innerText(),'# 代码原样\n');
  27. assert.equal(await page.locator('.message.user').innerText(),'# 用户输入');
  28. await page.evaluate(() => appendMessage('assistant','<img src=x onerror="window.bad=1"><script>window.bad=1</script>[危险](javascript:alert(1))\n\n[来源](https://example.com)'));
  29. assert.equal(await page.locator('.markdown-body script, .markdown-body img, .markdown-body [onerror], .markdown-body a[href^="javascript:"]').count(),0);
  30. assert.equal(await page.evaluate(() => window.bad),undefined);
  31. // Send encoded SSE bytes in small chunks, including splits inside Chinese characters.
  32. await page.evaluate(async () => {
  33. const events = [['answer_chunk',{text:'# 分'}],['answer_chunk',{text:'块标题\n\n**粗'}],['answer_chunk',{text:'体**'}],['done',{answer:'# 最终标题\n\n**完整答案**'}]];
  34. const bytes = new TextEncoder().encode(events.map(([event,data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`).join(''));
  35. const original = window.fetch;
  36. window.fetch = async () => new Response(new ReadableStream({start(c) {for(let i=0;i<bytes.length;i+=7)c.enqueue(bytes.slice(i,i+7)); c.close();}}));
  37. try { await askStream('测试',true,false); } finally { window.fetch=original; }
  38. });
  39. assert.equal(await page.locator('.answer-text h1').innerText(),'最终标题');
  40. assert.equal(await page.locator('.answer-text strong').innerText(),'完整答案');
  41. await page.evaluate(async () => {
  42. const original=window.fetch;
  43. window.fetch=async () => new Response(JSON.stringify({status:'ok',answer:'### 确认后的回答\n\n1. 第一项'}));
  44. try { await resumeThread('确认'); } finally { window.fetch=original; }
  45. });
  46. assert.equal(await page.locator('.markdown-body h3').innerText(),'确认后的回答');
  47. await page.evaluate(() => showConfirm('请选择', ['1. 水电工', '2. 维修电工', '都不是'], 'clarify_value'));
  48. await page.locator('#confirmOptions .option-chip').nth(0).click();
  49. await page.locator('#confirmOptions .option-chip').nth(1).click();
  50. assert.equal(await page.locator('#confirmOptions .option-chip.selected').count(),2);
  51. assert.equal(await page.locator('#confirmReply').inputValue(),'1. 水电工,2. 维修电工');
  52. await page.locator('#confirmOptions .option-chip').nth(2).click();
  53. assert.equal(await page.locator('#confirmOptions .option-chip.selected').count(),1);
  54. assert.equal(await page.locator('#confirmReply').inputValue(),'都不是');
  55. await page.evaluate(() => showConfirm('普通确认', ['确认', '修改'], 'confirm_plan'));
  56. await page.locator('#confirmOptions .option-chip').nth(0).click();
  57. await page.locator('#confirmOptions .option-chip').nth(1).click();
  58. assert.equal(await page.locator('#confirmReply').inputValue(),'修改');
  59. await page.setViewportSize({width:390,height:844});
  60. assert.ok(await page.locator('.markdown-body h1').first().isVisible());
  61. console.log('PASS: headings, lists, table, code, user text, XSS filtering, SSE UTF-8 chunks/final replacement, resume, candidate multi-select and mobile');
  62. } finally { await browser.close(); }
  63. })().catch(e => {console.error(e);process.exitCode=1;});