mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-29 01:03:51 +08:00
Add browser-based unit tests for xiaozhi test modules
- Add browser-compatible test files (no npm required) - recorder.test.browser.js: 8 tests for microphone and HTTP detection - tools.test.browser.js: 5 tests for Live2D actions and error handling - Add test runner (test-runner.html) with built-in test framework - Add null safety checks in tools.js for DOM element access - Add documentation (English and Chinese versions) - README_TESTS.md / README_TESTS_CN.md: Complete test guide - QUICK_START_TEST.md / QUICK_START_TEST_CN.md: Quick start guides - Total: 13 unit tests covering microphone detection, HTTP detection, Live2D actions, and error handling
This commit is contained in:
@@ -38,7 +38,10 @@ export async function initMcpTools() {
|
||||
}
|
||||
|
||||
renderMcpTools();
|
||||
setupMcpEventListeners();
|
||||
// Only setup event listeners if DOM elements exist
|
||||
if (document.getElementById('toggleMcpTools')) {
|
||||
setupMcpEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +51,10 @@ function renderMcpTools() {
|
||||
const container = document.getElementById('mcpToolsContainer');
|
||||
const countSpan = document.getElementById('mcpToolsCount');
|
||||
|
||||
if (!container) {
|
||||
return; // Container not found, skip rendering
|
||||
}
|
||||
|
||||
if (countSpan) {
|
||||
countSpan.textContent = `${mcpTools.length} 个工具`;
|
||||
}
|
||||
@@ -97,6 +104,10 @@ function renderMcpTools() {
|
||||
function renderMcpProperties() {
|
||||
const container = document.getElementById('mcpPropertiesContainer');
|
||||
|
||||
if (!container) {
|
||||
return; // Container not found, skip rendering
|
||||
}
|
||||
|
||||
if (mcpProperties.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; padding: 20px; color: #999; font-size: 14px;">暂无参数,点击下方按钮添加参数</div>';
|
||||
return;
|
||||
@@ -213,6 +224,11 @@ function setupMcpEventListeners() {
|
||||
const form = document.getElementById('mcpToolForm');
|
||||
const addPropertyBtn = document.getElementById('addMcpPropertyBtn');
|
||||
|
||||
// Return early if required elements don't exist (e.g., in test environment)
|
||||
if (!toggleBtn || !panel || !addBtn || !modal || !closeBtn || !cancelBtn || !form || !addPropertyBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const isExpanded = panel.classList.contains('expanded');
|
||||
panel.classList.toggle('expanded');
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* MCP工具模块测试 - Browser compatible version
|
||||
* 测试Live2D动作工具执行功能
|
||||
*
|
||||
* This version works without Vitest - uses the simple test framework from test-runner.html
|
||||
*/
|
||||
|
||||
import { executeMcpTool, initMcpTools, setWebSocket } from './tools.js';
|
||||
|
||||
describe('Live2D Action Tools', () => {
|
||||
let mockLive2DManager;
|
||||
let originalChatApp;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Save original chatApp
|
||||
originalChatApp = window.chatApp;
|
||||
|
||||
// Mock Live2D manager
|
||||
mockLive2DManager = {
|
||||
motion: vi.fn()
|
||||
};
|
||||
|
||||
// Setup window.chatApp
|
||||
window.chatApp = {
|
||||
live2dManager: mockLive2DManager
|
||||
};
|
||||
|
||||
// Mock localStorage
|
||||
localStorage.getItem = vi.fn(() => null);
|
||||
|
||||
// Mock fetch for default-mcp-tools.json
|
||||
globalThis.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve([
|
||||
{
|
||||
name: 'live2d.smile',
|
||||
description: 'Make the virtual human smile',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
name: 'live2d.wave',
|
||||
description: 'Make the virtual human wave',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
name: 'live2d.action',
|
||||
description: 'Trigger a specified action',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: { type: 'string' }
|
||||
},
|
||||
required: ['action']
|
||||
}
|
||||
}
|
||||
])
|
||||
})
|
||||
);
|
||||
|
||||
// Mock DOM elements - ensure all required elements exist
|
||||
const mockContainer = {
|
||||
innerHTML: '',
|
||||
appendChild: vi.fn(),
|
||||
textContent: ''
|
||||
};
|
||||
|
||||
document.getElementById = vi.fn((id) => {
|
||||
// Return mock elements for all IDs that tools.js might access
|
||||
if (id === 'mcpToolsContainer' || id === 'mcpPropertiesContainer') {
|
||||
return mockContainer;
|
||||
}
|
||||
if (id === 'mcpToolsCount') {
|
||||
return { textContent: '' };
|
||||
}
|
||||
// Return null for other elements (they're checked with if statements)
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up
|
||||
window.chatApp = originalChatApp;
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试 executeMcpTool - smile 动作
|
||||
*/
|
||||
test('should execute Live2D smile action', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
const result = executeMcpTool('live2d.smile', {});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.action).toBe('FlickUp');
|
||||
expect(result.tool).toBe('live2d.smile');
|
||||
expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickUp');
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试 executeMcpTool - wave 动作
|
||||
*/
|
||||
test('should execute Live2D wave action', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
const result = executeMcpTool('live2d.wave', {});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.action).toBe('Tap');
|
||||
expect(result.tool).toBe('live2d.wave');
|
||||
expect(mockLive2DManager.motion).toHaveBeenCalledWith('Tap');
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试 executeMcpTool - 通用动作工具
|
||||
*/
|
||||
test('should handle generic action tool', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
const result = executeMcpTool('live2d.action', { action: 'FlickDown' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.action).toBe('FlickDown');
|
||||
expect(result.tool).toBe('live2d.action');
|
||||
expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickDown');
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试 executeMcpTool - Live2D管理器未初始化
|
||||
*/
|
||||
test('should handle missing Live2D manager gracefully', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
window.chatApp = null;
|
||||
|
||||
const result = executeMcpTool('live2d.smile', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Live2D管理器未初始化');
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试 executeMcpTool - 未知的工具
|
||||
*/
|
||||
test('should handle unknown tool gracefully', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
const result = executeMcpTool('unknown.tool', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('未知工具');
|
||||
});
|
||||
});
|
||||
@@ -3,81 +3,218 @@
|
||||
* 测试Live2D动作工具执行功能
|
||||
*/
|
||||
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { executeMcpTool, initMcpTools, setWebSocket } from './tools.js';
|
||||
|
||||
describe('Live2D Action Tools', () => {
|
||||
let mockLive2DManager;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock Live2D manager
|
||||
mockLive2DManager = {
|
||||
motion: vi.fn()
|
||||
};
|
||||
|
||||
// Setup window.chatApp
|
||||
global.window.chatApp = {
|
||||
live2dManager: mockLive2DManager
|
||||
};
|
||||
|
||||
// Mock localStorage
|
||||
global.localStorage.getItem = vi.fn(() => null);
|
||||
|
||||
// Mock fetch for default-mcp-tools.json
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve([
|
||||
{
|
||||
name: 'live2d.smile',
|
||||
description: 'Make the virtual human smile',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
name: 'live2d.wave',
|
||||
description: 'Make the virtual human wave',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
name: 'live2d.action',
|
||||
description: 'Trigger a specified action',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: { type: 'string' }
|
||||
},
|
||||
required: ['action']
|
||||
}
|
||||
}
|
||||
])
|
||||
})
|
||||
);
|
||||
|
||||
// Mock DOM elements
|
||||
global.document.getElementById = vi.fn((id) => {
|
||||
if (id === 'mcpToolsContainer') {
|
||||
return {
|
||||
innerHTML: '',
|
||||
appendChild: vi.fn()
|
||||
};
|
||||
}
|
||||
if (id === 'mcpToolsCount') {
|
||||
return {
|
||||
textContent: ''
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up
|
||||
global.window.chatApp = null;
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试 executeLive2DAction 函数
|
||||
* 注意:需要 mock window.chatApp.live2dManager
|
||||
* 测试 executeMcpTool - smile 动作
|
||||
*/
|
||||
test('should execute Live2D smile action', () => {
|
||||
// Mock Live2D manager
|
||||
const mockLive2DManager = {
|
||||
motion: jest.fn()
|
||||
};
|
||||
test('should execute Live2D smile action', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
window.chatApp = {
|
||||
live2dManager: mockLive2DManager
|
||||
};
|
||||
|
||||
// 测试 smile 动作
|
||||
// const result = executeLive2DAction('live2d.smile', {});
|
||||
// expect(result.success).toBe(true);
|
||||
// expect(result.action).toBe('FlickUp');
|
||||
// expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickUp');
|
||||
const result = executeMcpTool('live2d.smile', {});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.action).toBe('FlickUp');
|
||||
expect(result.tool).toBe('live2d.smile');
|
||||
expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickUp');
|
||||
});
|
||||
|
||||
test('should execute Live2D wave action', () => {
|
||||
// Mock Live2D manager
|
||||
const mockLive2DManager = {
|
||||
motion: jest.fn()
|
||||
};
|
||||
/**
|
||||
* 测试 executeMcpTool - wave 动作
|
||||
*/
|
||||
test('should execute Live2D wave action', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
window.chatApp = {
|
||||
live2dManager: mockLive2DManager
|
||||
};
|
||||
|
||||
// 测试 wave 动作
|
||||
// const result = executeLive2DAction('live2d.wave', {});
|
||||
// expect(result.success).toBe(true);
|
||||
// expect(result.action).toBe('Tap');
|
||||
// expect(mockLive2DManager.motion).toHaveBeenCalledWith('Tap');
|
||||
const result = executeMcpTool('live2d.wave', {});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.action).toBe('Tap');
|
||||
expect(result.tool).toBe('live2d.wave');
|
||||
expect(mockLive2DManager.motion).toHaveBeenCalledWith('Tap');
|
||||
});
|
||||
|
||||
test('should handle generic action tool', () => {
|
||||
// Mock Live2D manager
|
||||
const mockLive2DManager = {
|
||||
motion: jest.fn()
|
||||
};
|
||||
/**
|
||||
* 测试 executeMcpTool - 通用动作工具
|
||||
*/
|
||||
test('should handle generic action tool', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
window.chatApp = {
|
||||
live2dManager: mockLive2DManager
|
||||
};
|
||||
|
||||
// 测试通用动作工具
|
||||
// const result = executeLive2DAction('live2d.action', { action: 'FlickDown' });
|
||||
// expect(result.success).toBe(true);
|
||||
// expect(result.action).toBe('FlickDown');
|
||||
// expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickDown');
|
||||
const result = executeMcpTool('live2d.action', { action: 'FlickDown' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.action).toBe('FlickDown');
|
||||
expect(result.tool).toBe('live2d.action');
|
||||
expect(mockLive2DManager.motion).toHaveBeenCalledWith('FlickDown');
|
||||
});
|
||||
|
||||
test('should handle missing Live2D manager gracefully', () => {
|
||||
window.chatApp = null;
|
||||
|
||||
// const result = executeLive2DAction('live2d.smile', {});
|
||||
// expect(result.success).toBe(false);
|
||||
// expect(result.error).toContain('Live2D管理器未初始化');
|
||||
/**
|
||||
* 测试 executeMcpTool - Live2D管理器未初始化
|
||||
*/
|
||||
test('should handle missing Live2D manager gracefully', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
global.window.chatApp = null;
|
||||
|
||||
const result = executeMcpTool('live2d.smile', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Live2D管理器未初始化');
|
||||
});
|
||||
|
||||
test('should handle unknown action gracefully', () => {
|
||||
const mockLive2DManager = {
|
||||
motion: jest.fn()
|
||||
};
|
||||
/**
|
||||
* 测试 executeMcpTool - 未知的动作
|
||||
*/
|
||||
test('should handle unknown action gracefully', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
window.chatApp = {
|
||||
live2dManager: mockLive2DManager
|
||||
};
|
||||
// Add an unknown live2d tool to the list
|
||||
const tools = await global.fetch().then(res => res.json());
|
||||
tools.push({
|
||||
name: 'live2d.unknown',
|
||||
description: 'Unknown action',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
});
|
||||
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve(tools)
|
||||
})
|
||||
);
|
||||
|
||||
await initMcpTools();
|
||||
|
||||
const result = executeMcpTool('live2d.unknown', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('未知的动作');
|
||||
});
|
||||
|
||||
// const result = executeLive2DAction('live2d.unknown', {});
|
||||
// expect(result.success).toBe(false);
|
||||
// expect(result.error).toContain('未知的动作');
|
||||
/**
|
||||
* 测试 executeMcpTool - 未知的工具
|
||||
*/
|
||||
test('should handle unknown tool gracefully', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
const result = executeMcpTool('unknown.tool', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('未知工具');
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试 executeMcpTool - 其他动作映射
|
||||
*/
|
||||
test('should handle other action mappings', async () => {
|
||||
await initMcpTools();
|
||||
|
||||
const actionMappings = [
|
||||
{ tool: 'live2d.happy', expectedAction: 'FlickUp' },
|
||||
{ tool: 'live2d.sad', expectedAction: 'FlickDown' },
|
||||
{ tool: 'live2d.tap', expectedAction: 'Tap' },
|
||||
{ tool: 'live2d.tapBody', expectedAction: 'Tap@Body' },
|
||||
{ tool: 'live2d.flick', expectedAction: 'Flick' },
|
||||
{ tool: 'live2d.flickBody', expectedAction: 'Flick@Body' },
|
||||
{ tool: 'live2d.flickUp', expectedAction: 'FlickUp' },
|
||||
{ tool: 'live2d.flickDown', expectedAction: 'FlickDown' }
|
||||
];
|
||||
|
||||
// Add these tools to the mock
|
||||
const tools = await global.fetch().then(res => res.json());
|
||||
actionMappings.forEach(mapping => {
|
||||
tools.push({
|
||||
name: mapping.tool,
|
||||
description: `Test ${mapping.tool}`,
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
});
|
||||
});
|
||||
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve(tools)
|
||||
})
|
||||
);
|
||||
|
||||
await initMcpTools();
|
||||
|
||||
for (const mapping of actionMappings) {
|
||||
mockLive2DManager.motion.mockClear();
|
||||
const result = executeMcpTool(mapping.tool, {});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.action).toBe(mapping.expectedAction);
|
||||
expect(mockLive2DManager.motion).toHaveBeenCalledWith(mapping.expectedAction);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user