| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374 |
- <!doctype html>
- <html lang="zh-CN"><meta charset="utf-8"><title>Markdown regression checks</title>
- <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>
- <h1>Markdown regression checks</h1><pre id="result">Running…</pre><iframe title="Frontend under test" src="/static/index.html"></iframe>
- <script>
- const frame = document.querySelector('iframe');
- frame.addEventListener('load', async () => {
- const w = frame.contentWindow;
- const results = [];
- const check = (condition, message) => { if (!condition) throw new Error(message); results.push('PASS ' + message); };
- const pause = () => new Promise(resolve => setTimeout(resolve, 80));
- const render = (source) => { const ui = w.newAssistant(); w.appendToken(ui, source); w.breakSegment(ui); return ui; };
- 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)';
- try {
- await pause();
- w.document.getElementById('newBtn').click();
- const full = render(sample);
- check(full.bubble.querySelector('h1')?.textContent === 'Markdown 标题', 'headings');
- check(full.bubble.querySelector('strong')?.textContent === '加粗' && full.bubble.querySelector('em') && full.bubble.querySelector('del'), 'inline formatting');
- check(full.bubble.querySelector('ul ul li') && full.bubble.querySelector('input:checked:disabled'), 'nested and task lists');
- check(full.bubble.querySelector('blockquote') && full.bubble.querySelector('.markdown-table table tbody td'), 'quotes and GFM tables');
- check(full.bubble.querySelector('pre code')?.textContent.includes(' print("<hello>")'), 'code indentation and literal HTML');
- check(full.bubble.querySelector('a')?.rel === 'noopener noreferrer', 'safe external links');
- const streamed = w.newAssistant();
- for (const char of sample) w.appendToken(streamed, char);
- await pause();
- check(streamed.bubble.innerHTML === full.bubble.innerHTML, 'fragmented tokens match complete Markdown');
- const mixed = w.newAssistant();
- w.appendToken(mixed, '**before**');
- const tool = w.addCardFor(mixed, 'done', '工具完成', '{"ok":true}');
- w.appendToken(mixed, '**after**');
- w.breakSegment(mixed);
- await pause();
- 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');
- const incomplete = render('```js\nconst value = "<tag>";');
- check(incomplete.bubble.querySelector('pre code')?.textContent.includes('<tag>'), 'unfinished code fences during streaming');
- 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="javascript:alert(1)">bad</a><iframe srcdoc="bad"></iframe>');
- check(!hostile.bubble.querySelector('script,style,svg,iframe,[onerror],[onload],a[href^="javascript:"]') && !w.__xss, 'HTML and dangerous URLs are sanitized');
- const purifier = w.DOMPurify;
- w.DOMPurify = undefined;
- const fallback = render('**safe fallback** <img onerror="bad()">');
- check(fallback.bubble.textContent.includes('**safe fallback**') && !fallback.bubble.querySelector('img'), 'missing dependency falls back to plain text');
- w.DOMPurify = purifier;
- const fetchOriginal = w.fetch;
- const stub = async (url) => {
- if (url.includes('/chat/history/')) return w.Response.json({items:[{msg_role:'assistant',content:sample}]});
- if (url.includes('/chat/interrupt')) return new w.Response(null,{status:204});
- if (url.includes('/sessions')) return w.Response.json({items:[]});
- return w.Response.json({model:'test'});
- };
- w.fetch = stub;
- await w.openSession({id:'markdown-history',title:'Markdown 历史消息',mode:'full'});
- check(w.document.querySelector('#log h1')?.textContent === 'Markdown 标题' && w.document.querySelector('#log table'), 'history uses the same Markdown renderer');
- const encode = content => new w.TextEncoder().encode('data: '+JSON.stringify({type:'token',content})+'\n\n');
- 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);
- await w.stream('/test-stream','POST',new w.FormData());
- check(w.document.querySelector('#log .assistant:last-child h2')?.textContent === '完成时刷新', 'stream completion flushes pending render');
- 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);
- const pending = w.stream('/test-abort','POST',new w.FormData());
- await pause();
- w.document.getElementById('stopBtn').click();
- await pending;
- check(w.document.querySelector('#log .assistant:last-child strong')?.textContent === '保留部分回复', 'stopping preserves partial Markdown');
- w.fetch = fetchOriginal;
- w.document.getElementById('newBtn').click();
- render(sample);
- document.getElementById('result').className = 'pass';
- document.getElementById('result').textContent = results.join('\n') + '\n\n' + results.length + ' checks passed';
- } catch (error) {
- document.getElementById('result').className = 'fail';
- document.getElementById('result').textContent = results.join('\n') + '\nFAIL ' + error.stack;
- }
- });
- </script></html>
|