frontend_markdown.html 5.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. <!doctype html>
  2. <html lang="zh-CN"><meta charset="utf-8"><title>Markdown regression checks</title>
  3. <style>body{font:14px system-ui;background:#0f1115;color:#e6e9ef;margin:24px}iframe{width:100%;height:620px;border:1px solid #2a2f3a}pre{white-space:pre-wrap}.pass{color:#34d399}.fail{color:#f87171}</style>
  4. <h1>Markdown regression checks</h1><pre id="result">Running…</pre><iframe title="Frontend under test" src="/static/index.html"></iframe>
  5. <script>
  6. const frame = document.querySelector('iframe');
  7. frame.addEventListener('load', async () => {
  8. const w = frame.contentWindow;
  9. const results = [];
  10. const check = (condition, message) => { if (!condition) throw new Error(message); results.push('PASS ' + message); };
  11. const pause = () => new Promise(resolve => setTimeout(resolve, 80));
  12. const render = (source) => { const ui = w.newAssistant(); w.appendToken(ui, source); w.breakSegment(ui); return ui; };
  13. const sample = '# Markdown 标题\n\n这是 **加粗**、*斜体*、~~删除线~~ 和 `inline code`。\n\n- 第一项\n - 嵌套项\n- [x] 已完成\n\n> 引用内容\n\n| 项目 | 数值 |\n| --- | ---: |\n| 风量 | 120 |\n\n```python\ndef greet():\n print("<hello>")\n```\n\n[文档](https://example.com)';
  14. try {
  15. await pause();
  16. w.document.getElementById('newBtn').click();
  17. const full = render(sample);
  18. check(full.bubble.querySelector('h1')?.textContent === 'Markdown 标题', 'headings');
  19. check(full.bubble.querySelector('strong')?.textContent === '加粗' && full.bubble.querySelector('em') && full.bubble.querySelector('del'), 'inline formatting');
  20. check(full.bubble.querySelector('ul ul li') && full.bubble.querySelector('input:checked:disabled'), 'nested and task lists');
  21. check(full.bubble.querySelector('blockquote') && full.bubble.querySelector('.markdown-table table tbody td'), 'quotes and GFM tables');
  22. check(full.bubble.querySelector('pre code')?.textContent.includes(' print("<hello>")'), 'code indentation and literal HTML');
  23. check(full.bubble.querySelector('a')?.rel === 'noopener noreferrer', 'safe external links');
  24. const streamed = w.newAssistant();
  25. for (const char of sample) w.appendToken(streamed, char);
  26. await pause();
  27. check(streamed.bubble.innerHTML === full.bubble.innerHTML, 'fragmented tokens match complete Markdown');
  28. const mixed = w.newAssistant();
  29. w.appendToken(mixed, '**before**');
  30. const tool = w.addCardFor(mixed, 'done', '工具完成', '{"ok":true}');
  31. w.appendToken(mixed, '**after**');
  32. w.breakSegment(mixed);
  33. await pause();
  34. check(mixed.bubble.children[1] === tool && mixed.bubble.children[0].querySelector('strong').textContent === 'before' && mixed.bubble.children[2].querySelector('strong').textContent === 'after', 'tool cards preserve segment order');
  35. const incomplete = render('```js\nconst value = "<tag>";');
  36. check(incomplete.bubble.querySelector('pre code')?.textContent.includes('<tag>'), 'unfinished code fences during streaming');
  37. const hostile = render('<img src="x" onerror="window.__xss=1"><script>window.__xss=1<\/script><svg onload="window.__xss=1"></svg><style>body{display:none}</style>[bad](javascript:alert%281%29)<a href="jav&#x61;script:alert(1)">bad</a><iframe srcdoc="bad"></iframe>');
  38. check(!hostile.bubble.querySelector('script,style,svg,iframe,[onerror],[onload],a[href^="javascript:"]') && !w.__xss, 'HTML and dangerous URLs are sanitized');
  39. const purifier = w.DOMPurify;
  40. w.DOMPurify = undefined;
  41. const fallback = render('**safe fallback** <img onerror="bad()">');
  42. check(fallback.bubble.textContent.includes('**safe fallback**') && !fallback.bubble.querySelector('img'), 'missing dependency falls back to plain text');
  43. w.DOMPurify = purifier;
  44. const fetchOriginal = w.fetch;
  45. const stub = async (url) => {
  46. if (url.includes('/chat/history/')) return w.Response.json({items:[{msg_role:'assistant',content:sample}]});
  47. if (url.includes('/chat/interrupt')) return new w.Response(null,{status:204});
  48. if (url.includes('/sessions')) return w.Response.json({items:[]});
  49. return w.Response.json({model:'test'});
  50. };
  51. w.fetch = stub;
  52. await w.openSession({id:'markdown-history',title:'Markdown 历史消息',mode:'full'});
  53. check(w.document.querySelector('#log h1')?.textContent === 'Markdown 标题' && w.document.querySelector('#log table'), 'history uses the same Markdown renderer');
  54. const encode = content => new w.TextEncoder().encode('data: '+JSON.stringify({type:'token',content})+'\n\n');
  55. w.fetch = async (url, options) => url === '/test-stream' ? new w.Response(new w.ReadableStream({start(c){c.enqueue(encode('## 完成时刷新\n\n**末尾**'));c.close();}})) : stub(url);
  56. await w.stream('/test-stream','POST',new w.FormData());
  57. check(w.document.querySelector('#log .assistant:last-child h2')?.textContent === '完成时刷新', 'stream completion flushes pending render');
  58. w.fetch = async (url, options) => url === '/test-abort' ? new w.Response(new w.ReadableStream({start(c){c.enqueue(encode('**保留部分回复**')); options.signal.addEventListener('abort',()=>c.error(new w.DOMException('Stopped','AbortError')));}})) : stub(url);
  59. const pending = w.stream('/test-abort','POST',new w.FormData());
  60. await pause();
  61. w.document.getElementById('stopBtn').click();
  62. await pending;
  63. check(w.document.querySelector('#log .assistant:last-child strong')?.textContent === '保留部分回复', 'stopping preserves partial Markdown');
  64. w.fetch = fetchOriginal;
  65. w.document.getElementById('newBtn').click();
  66. render(sample);
  67. document.getElementById('result').className = 'pass';
  68. document.getElementById('result').textContent = results.join('\n') + '\n\n' + results.length + ' checks passed';
  69. } catch (error) {
  70. document.getElementById('result').className = 'fail';
  71. document.getElementById('result').textContent = results.join('\n') + '\nFAIL ' + error.stack;
  72. }
  73. });
  74. </script></html>