Files
xiaozhi-esp32-server/main/xiaozhi-server/test/test-runner.html
T

59 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Unit Tests Runner</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);min-height:100vh;padding:20px}.container{max-width:1200px;margin:0 auto;background:white;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,0.2);overflow:hidden}.header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:30px;text-align:center}.header h1{font-size:2.5em;margin-bottom:10px}.header p{opacity:0.9;font-size:1.1em}.controls{padding:20px;background:#f5f5f5;border-bottom:2px solid #e0e0e0;display:flex;gap:10px;flex-wrap:wrap;align-items:center}button{padding:12px 24px;border:none;border-radius:6px;font-size:16px;cursor:pointer;transition:all 0.3s;font-weight:600}.btn-run{background:#4CAF50;color:white}.btn-run:hover{background:#45a049;transform:translateY(-2px);box-shadow:0 4px 8px rgba(0,0,0,0.2)}.btn-clear{background:#f44336;color:white}.btn-clear:hover{background:#da190b}.stats{display:flex;gap:20px;margin-left:auto;font-size:14px;color:#666}.stat{padding:8px 16px;background:white;border-radius:4px;border:1px solid #ddd}.stat-value{font-weight:bold;font-size:18px;color:#333}.results{padding:20px;max-height:600px;overflow-y:auto}.test-suite{margin-bottom:30px;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}.test-suite-header{background:#f9f9f9;padding:15px 20px;font-weight:bold;font-size:18px;border-bottom:2px solid #e0e0e0}.test-case{padding:15px 20px;border-bottom:1px solid #f0f0f0;transition:background 0.2s}.test-case:hover{background:#f9f9f9}.test-case:last-child{border-bottom:none}.test-name{font-weight:600;margin-bottom:8px;display:flex;align-items:center;gap:10px}.test-status{display:inline-block;width:12px;height:12px;border-radius:50%}.test-status.pending{background:#ffa500}.test-status.pass{background:#4CAF50}.test-status.fail{background:#f44336}.test-error{margin-top:8px;padding:10px;background:#ffebee;border-left:4px solid #f44336;border-radius:4px;color:#c62828;font-family:'Courier New',monospace;font-size:13px;white-space:pre-wrap}.test-duration{color:#999;font-size:12px;margin-left:auto}.summary{padding:20px;background:#f5f5f5;border-top:2px solid #e0e0e0;text-align:center;font-size:18px}.summary.pass{color:#4CAF50;font-weight:bold}.summary.fail{color:#f44336;font-weight:bold}.loading{text-align:center;padding:40px;color:#666}.spinner{border:4px solid #f3f3f3;border-top:4px solid #667eea;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite;margin:0 auto 20px}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 Unit Tests Runner</h1>
<p>Browser-based test runner for xiaozhi test modules</p>
</div>
<div class="controls">
<button class="btn-run" onclick="runTests()">▶ Run All Tests</button>
<button class="btn-clear" onclick="clearResults()">🗑️ Clear Results</button>
<div class="stats">
<div class="stat"><div>Total</div><div class="stat-value" id="stat-total">0</div></div>
<div class="stat"><div>Passed</div><div class="stat-value" id="stat-passed" style="color:#4CAF50">0</div></div>
<div class="stat"><div>Failed</div><div class="stat-value" id="stat-failed" style="color:#f44336">0</div></div>
</div>
</div>
<div class="results" id="results">
<div class="loading" style="display:none" id="loading">
<div class="spinner"></div>
<div>Running tests...</div>
</div>
<div style="text-align:center;padding:40px;color:#999">Click "Run All Tests" to start testing</div>
</div>
<div class="summary" id="summary"></div>
</div>
<script type="module">
const mockElement={innerHTML:'',textContent:'',appendChild:()=>{},addEventListener:()=>{},classList:{add:()=>{},remove:()=>{},toggle:()=>{},contains:()=>false},style:{}};
const originalGetElementById=document.getElementById.bind(document);
document.getElementById=function(id){const realElement=originalGetElementById(id);return realElement||mockElement;};
if(!navigator.mediaDevices){navigator.mediaDevices={getUserMedia:()=>Promise.reject(new Error('Not implemented'))};}
const testResults={suites:[],total:0,passed:0,failed:0};
globalThis.describe=function(name,fn){testResults.suites.push({name,tests:[],fn});fn();};
globalThis.test=function(name,fn){const suite=testResults.suites[testResults.suites.length-1];if(suite){suite.tests.push({name,fn,status:'pending',error:null,duration:0});testResults.total++;}};
globalThis.expect=function(actual){return{toBe(expected){if(actual!==expected)throw new Error(`Expected ${expected}, but got ${actual}`);},toHaveBeenCalled(){if(!actual||typeof actual!=='function'||!actual.mock||actual.mock.calls.length===0)throw new Error('Expected function to have been called');},toHaveBeenCalledWith(...args){if(!actual||typeof actual!=='function'||!actual.mock)throw new Error('Expected function to have been called');const deepEqual=(a,b)=>{if(a===b)return true;if(a==null||b==null)return false;if(typeof a!=='object'||typeof b!=='object')return false;const keysA=Object.keys(a),keysB=Object.keys(b);if(keysA.length!==keysB.length)return false;for(const key of keysA){if(!keysB.includes(key))return false;if(!deepEqual(a[key],b[key]))return false;}return true;};const found=actual.mock.calls.some(call=>call.length===args.length&&call.every((val,i)=>deepEqual(val,args[i])));if(!found)throw new Error(`Expected function to have been called with ${JSON.stringify(args)}, but was called with: ${actual.mock.calls.map(c=>JSON.stringify(c)).join(', ')}`);},toContain(substring){if(typeof actual!=='string'||!actual.includes(substring))throw new Error(`Expected string to contain "${substring}"`);}};};
globalThis.vi={fn:function(impl){const mockData={calls:[],resolvedValue:undefined,rejectedValue:undefined};const mock=function(...args){mockData.calls.push(args);if(mockData.rejectedValue!==undefined)return Promise.reject(mockData.rejectedValue);if(mockData.resolvedValue!==undefined)return Promise.resolve(mockData.resolvedValue);return impl?impl(...args):undefined;};mock.mock=mockData;mock.mockResolvedValue=function(value){mockData.resolvedValue=value;mockData.rejectedValue=undefined;return mock;};mock.mockRejectedValue=function(value){mockData.rejectedValue=value;mockData.resolvedValue=undefined;return mock;};return mock;},clearAllMocks:()=>{}};
globalThis.beforeEach=function(fn){const suite=testResults.suites[testResults.suites.length-1];if(suite)suite.beforeEach=fn;};
globalThis.afterEach=function(fn){const suite=testResults.suites[testResults.suites.length-1];if(suite)suite.afterEach=fn;};
async function runTests(){const resultsDiv=document.querySelector('#results'),loadingDiv=document.querySelector('#loading'),summaryDiv=document.querySelector('#summary');if(!resultsDiv||!loadingDiv||!summaryDiv){alert('Error: Test runner UI elements not found. Please refresh the page.');return;}
testResults.suites=[];testResults.total=0;testResults.passed=0;testResults.failed=0;loadingDiv.style.display='block';resultsDiv.innerHTML='';summaryDiv.textContent='';
try{await import('./js/core/audio/recorder.test.browser.js');await import('./js/core/mcp/tools.test.browser.js');
for(const suite of testResults.suites){for(const testCase of suite.tests){const startTime=performance.now();try{document.getElementById=()=>mockElement;if(suite.beforeEach)await suite.beforeEach();await testCase.fn();testCase.status='pass';testResults.passed++;}catch(error){testCase.status='fail';testCase.error=error.message||String(error);testResults.failed++;}finally{testCase.duration=performance.now()-startTime;if(suite.afterEach){try{await suite.afterEach();}catch(e){}}}}}
renderResults();}catch(error){const errorDiv=document.querySelector('#results');if(errorDiv)errorDiv.innerHTML=`<div class="test-error">Error loading tests: ${error.message}</div>`;}finally{const loadingDivFinal=document.querySelector('#loading');if(loadingDivFinal)loadingDivFinal.style.display='none';}}
function renderResults(){const resultsDiv=document.querySelector('#results'),summaryDiv=document.querySelector('#summary');if(!resultsDiv||!summaryDiv)return;let html='';for(const suite of testResults.suites){html+=`<div class="test-suite"><div class="test-suite-header">${suite.name}</div>`;for(const testCase of suite.tests){html+=`<div class="test-case"><div class="test-name"><span class="test-status ${testCase.status}"></span><span>${testCase.name}</span><span class="test-duration">${testCase.duration.toFixed(2)}ms</span></div>`;if(testCase.error)html+=`<div class="test-error">${testCase.error}</div>`;html+=`</div>`;}html+=`</div>`;}
resultsDiv.innerHTML=html;const statTotal=document.querySelector('#stat-total'),statPassed=document.querySelector('#stat-passed'),statFailed=document.querySelector('#stat-failed');if(statTotal)statTotal.textContent=testResults.total;if(statPassed)statPassed.textContent=testResults.passed;if(statFailed)statFailed.textContent=testResults.failed;
if(testResults.failed===0){summaryDiv.className='summary pass';summaryDiv.textContent=`✅ All ${testResults.total} tests passed!`;}else{summaryDiv.className='summary fail';summaryDiv.textContent=`❌ ${testResults.failed} of ${testResults.total} tests failed`;}}
function clearResults(){const resultsDiv=document.querySelector('#results'),summaryDiv=document.querySelector('#summary'),statTotal=document.querySelector('#stat-total'),statPassed=document.querySelector('#stat-passed'),statFailed=document.querySelector('#stat-failed');if(resultsDiv)resultsDiv.innerHTML='<div style="text-align:center;padding:40px;color:#999">Click "Run All Tests" to start testing</div>';if(summaryDiv)summaryDiv.textContent='';if(statTotal)statTotal.textContent='0';if(statPassed)statPassed.textContent='0';if(statFailed)statFailed.textContent='0';}
window.runTests=runTests;window.clearResults=clearResults;window._originalGetElementById=originalGetElementById;
</script>
</body>
</html>