2026-01-26 14:37:07 -08:00
<!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 ( 135 deg , #667eea 0 % , #764ba2 100 % ); min-height : 100 vh ; padding : 20 px }. container { max-width : 1200 px ; margin : 0 auto ; background : white ; border-radius : 12 px ; box-shadow : 0 10 px 40 px rgba ( 0 , 0 , 0 , 0.2 ); overflow : hidden }. header { background : linear-gradient ( 135 deg , #667eea 0 % , #764ba2 100 % ); color : white ; padding : 30 px ; text-align : center }. header h1 { font-size : 2.5 em ; margin-bottom : 10 px }. header p { opacity : 0.9 ; font-size : 1.1 em }. controls { padding : 20 px ; background : #f5f5f5 ; border-bottom : 2 px solid #e0e0e0 ; display : flex ; gap : 10 px ; flex-wrap : wrap ; align-items : center } button { padding : 12 px 24 px ; border : none ; border-radius : 6 px ; font-size : 16 px ; cursor : pointer ; transition : all 0.3 s ; font-weight : 600 }. btn-run { background : #4CAF50 ; color : white }. btn-run : hover { background : #45a049 ; transform : translateY ( -2 px ); box-shadow : 0 4 px 8 px rgba ( 0 , 0 , 0 , 0.2 )}. btn-clear { background : #f44336 ; color : white }. btn-clear : hover { background : #da190b }. stats { display : flex ; gap : 20 px ; margin-left : auto ; font-size : 14 px ; color : #666 }. stat { padding : 8 px 16 px ; background : white ; border-radius : 4 px ; border : 1 px solid #ddd }. stat-value { font-weight : bold ; font-size : 18 px ; color : #333 }. results { padding : 20 px ; max-height : 600 px ; overflow-y : auto }. test-suite { margin-bottom : 30 px ; border : 1 px solid #e0e0e0 ; border-radius : 8 px ; overflow : hidden }. test-suite-header { background : #f9f9f9 ; padding : 15 px 20 px ; font-weight : bold ; font-size : 18 px ; border-bottom : 2 px solid #e0e0e0 }. test-case { padding : 15 px 20 px ; border-bottom : 1 px solid #f0f0f0 ; transition : background 0.2 s }. test-case : hover { background : #f9f9f9 }. test-case : last-child { border-bottom : none }. test-name { font-weight : 600 ; margin-bottom : 8 px ; display : flex ; align-items : center ; gap : 10 px }. test-status { display : inline-block ; width : 12 px ; height : 12 px ; border-radius : 50 % }. test-status . pending { background : #ffa500 }. test-status . pass { background : #4CAF50 }. test-status . fail { background : #f44336 }. test-error { margin-top : 8 px ; padding : 10 px ; background : #ffebee ; border-left : 4 px solid #f44336 ; border-radius : 4 px ; color : #c62828 ; font-family : 'Courier New' , monospace ; font-size : 13 px ; white-space : pre-wrap }. test-duration { color : #999 ; font-size : 12 px ; margin-left : auto }. summary { padding : 20 px ; background : #f5f5f5 ; border-top : 2 px solid #e0e0e0 ; text-align : center ; font-size : 18 px }. summary . pass { color : #4CAF50 ; font-weight : bold }. summary . fail { color : #f44336 ; font-weight : bold }. loading { text-align : center ; padding : 40 px ; color : #666 }. spinner { border : 4 px solid #f3f3f3 ; border-top : 4 px solid #667eea ; border-radius : 50 % ; width : 40 px ; height : 40 px ; animation : spin 1 s linear infinite ; margin : 0 auto 20 px }@ keyframes spin { 0 % { transform : rotate ( 0 deg )} 100 % { transform : rotate ( 360 deg )}}
</ 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 >