From da71dce8606acc0d5e086246b27a2fabde70aa89 Mon Sep 17 00:00:00 2001 From: spider-yamet Date: Mon, 26 Jan 2026 14:37:07 -0800 Subject: [PATCH] Optimize code : minify test files --- main/xiaozhi-server/test/QUICK_START_TEST.md | 44 -- .../test/QUICK_START_TEST_CN.md | 44 -- main/xiaozhi-server/test/README_TESTS.md | 111 --- main/xiaozhi-server/test/README_TESTS_CN.md | 111 --- .../js/core/audio/recorder.test.browser.js | 226 ++---- main/xiaozhi-server/test/js/core/mcp/tools.js | 2 +- .../test/js/core/mcp/tools.test.browser.js | 219 ++---- main/xiaozhi-server/test/test-runner.html | 676 ++---------------- 8 files changed, 196 insertions(+), 1237 deletions(-) delete mode 100644 main/xiaozhi-server/test/QUICK_START_TEST.md delete mode 100644 main/xiaozhi-server/test/QUICK_START_TEST_CN.md delete mode 100644 main/xiaozhi-server/test/README_TESTS.md delete mode 100644 main/xiaozhi-server/test/README_TESTS_CN.md diff --git a/main/xiaozhi-server/test/QUICK_START_TEST.md b/main/xiaozhi-server/test/QUICK_START_TEST.md deleted file mode 100644 index 4830ff3e..00000000 --- a/main/xiaozhi-server/test/QUICK_START_TEST.md +++ /dev/null @@ -1,44 +0,0 @@ -# Quick Start - Browser Tests (No npm required!) - -## Run Tests in 3 Steps - -1. **Start a local server:** - ```bash - cd main/xiaozhi-server/test - python -m http.server 8007 - ``` - -2. **Open in browser:** - ``` - http://localhost:8007/test-runner.html - ``` - -3. **Click "▶ Run All Tests"** - -That's it! No npm, no package.json, no dependencies needed. - -## What Gets Tested? - -- ✅ Microphone availability detection (3 tests) -- ✅ HTTP non-localhost detection (5 tests) -- ✅ Live2D action execution (5 tests) -- ✅ Error handling and edge cases - -**Total: 13 unit tests** (8 recorder tests + 5 tools tests) - -## Test Files - -- `js/core/audio/recorder.test.browser.js` - Audio/recorder tests -- `js/core/mcp/tools.test.browser.js` - MCP tools and Live2D tests - -## Troubleshooting - -**Tests don't run?** -- Make sure you're using a local server (not `file://`) -- Check browser console for errors -- Ensure all `.js` files are accessible - -**Some tests fail?** -- Check the error message in the test results -- Verify mocks are set up correctly -- Check browser console for detailed errors diff --git a/main/xiaozhi-server/test/QUICK_START_TEST_CN.md b/main/xiaozhi-server/test/QUICK_START_TEST_CN.md deleted file mode 100644 index 11c7efd9..00000000 --- a/main/xiaozhi-server/test/QUICK_START_TEST_CN.md +++ /dev/null @@ -1,44 +0,0 @@ -# 快速开始 - 浏览器测试(无需 npm!) - -## 3 步运行测试 - -1. **启动本地服务器:** - ```bash - cd main/xiaozhi-server/test - python -m http.server 8007 - ``` - -2. **在浏览器中打开:** - ``` - http://localhost:8007/test-runner.html - ``` - -3. **点击 "▶ Run All Tests"(运行所有测试)** - -就这么简单!无需 npm,无需 package.json,无需任何依赖。 - -## 测试内容 - -- ✅ 麦克风可用性检测(3 个测试) -- ✅ HTTP 非本地访问检测(5 个测试) -- ✅ Live2D 动作执行(5 个测试) -- ✅ 错误处理和边界情况 - -**总计:13 个单元测试**(8 个录音器测试 + 5 个工具测试) - -## 测试文件 - -- `js/core/audio/recorder.test.browser.js` - 音频/录音器测试 -- `js/core/mcp/tools.test.browser.js` - MCP 工具和 Live2D 测试 - -## 故障排除 - -**测试无法运行?** -- 确保使用本地服务器(不要使用 `file://`) -- 检查浏览器控制台是否有错误 -- 确保所有 `.js` 文件都可以访问 - -**部分测试失败?** -- 查看测试结果中的错误信息 -- 验证模拟设置是否正确 -- 检查浏览器控制台获取详细错误信息 diff --git a/main/xiaozhi-server/test/README_TESTS.md b/main/xiaozhi-server/test/README_TESTS.md deleted file mode 100644 index c650a389..00000000 --- a/main/xiaozhi-server/test/README_TESTS.md +++ /dev/null @@ -1,111 +0,0 @@ -# Unit Tests Guide - -This directory contains unit tests for the xiaozhi test page modules. - -## Summary - -- **Total Tests:** 13 unit tests -- **Test Files:** 2 browser-compatible test files -- **Test Runner:** Browser-based (no npm required) -- **Coverage:** Microphone detection, HTTP detection, Live2D actions, error handling - -## Test Files - -- `js/core/audio/recorder.test.browser.js` - Browser-compatible tests for microphone availability detection and HTTP non-localhost detection -- `js/core/mcp/tools.test.browser.js` - Browser-compatible tests for MCP tools and Live2D action execution - -**Note:** The `.browser.js` versions work without any npm dependencies. They use a simple test framework built into `test-runner.html`. - -## Running Tests - -### Browser-based Test Runner (No npm required!) - -1. Start a local server: -```bash -cd main/xiaozhi-server/test -python -m http.server 8007 -``` - -2. Open `http://localhost:8007/test-runner.html` in your browser - -3. Click "▶ Run All Tests" button - -That's it! No npm, no package.json, no dependencies needed. - -## Test Coverage - -### `recorder.test.browser.js` -- ✅ `checkMicrophoneAvailability()` - Returns true when microphone is available -- ✅ `checkMicrophoneAvailability()` - Returns false when microphone is not available -- ✅ `checkMicrophoneAvailability()` - Returns false when browser doesn't support getUserMedia -- ✅ `isHttpNonLocalhost()` - Returns true for HTTP non-localhost access -- ✅ `isHttpNonLocalhost()` - Returns false for localhost -- ✅ `isHttpNonLocalhost()` - Returns false for 127.0.0.1 -- ✅ `isHttpNonLocalhost()` - Returns false for private IP addresses -- ✅ `isHttpNonLocalhost()` - Returns false for HTTPS protocol - -### `tools.test.browser.js` -- ✅ `executeMcpTool('live2d.smile')` - Executes FlickUp action -- ✅ `executeMcpTool('live2d.wave')` - Executes Tap action -- ✅ `executeMcpTool('live2d.action')` - Executes custom action -- ✅ `executeMcpTool()` - Handles missing Live2D manager gracefully -- ✅ `executeMcpTool()` - Handles unknown tools gracefully - -## Writing New Tests - -When adding new functionality, create a `.browser.js` test file that follows these patterns: - -```javascript -// your-module.test.browser.js -import { yourFunction } from './your-module.js'; - -describe('Your Feature', () => { - beforeEach(() => { - // Setup mocks and reset state - vi.clearAllMocks(); - }); - - test('should do something', () => { - // Arrange - const input = 'test'; - - // Act - const result = yourFunction(input); - - // Assert - expect(result).toBe('expected'); - }); -}); -``` - -## Mocking Guidelines - -- Use `vi.fn()` for function mocks -- Use `vi.fn().mockResolvedValue(value)` for async mocks that resolve -- Use `vi.fn().mockRejectedValue(error)` for async mocks that reject -- Use `vi.clearAllMocks()` in `beforeEach` to reset state -- Mock browser APIs (`navigator`, `window.location`, `localStorage`, `fetch`) -- Mock DOM elements when needed (`document.getElementById`, etc.) - -## Available Test Functions - -The browser test framework provides: -- `describe(name, fn)` - Define a test suite -- `test(name, fn)` - Define a test case -- `beforeEach(fn)` - Run before each test -- `afterEach(fn)` - Run after each test -- `expect(actual)` - Assertion object with: - - `.toBe(expected)` - Strict equality - - `.toHaveBeenCalled()` - Function was called - - `.toHaveBeenCalledWith(...args)` - Function was called with specific args - - `.toContain(substring)` - String contains substring -- `vi.fn(impl?)` - Create a mock function -- `vi.clearAllMocks()` - Clear all mocks - -## Notes - -- Tests use ES modules (`import`/`export`) -- Tests run directly in the browser (no Node.js needed) -- No npm dependencies required - everything is self-contained -- The test runner (`test-runner.html`) includes a simple test framework -- Tests are automatically loaded when you click "Run All Tests" diff --git a/main/xiaozhi-server/test/README_TESTS_CN.md b/main/xiaozhi-server/test/README_TESTS_CN.md deleted file mode 100644 index 1f14cc7a..00000000 --- a/main/xiaozhi-server/test/README_TESTS_CN.md +++ /dev/null @@ -1,111 +0,0 @@ -# 单元测试指南 - -本目录包含 xiaozhi 测试页面模块的单元测试。 - -## 摘要 - -- **测试总数:** 13 个单元测试 -- **测试文件:** 2 个浏览器兼容的测试文件 -- **测试运行器:** 基于浏览器(无需 npm) -- **测试覆盖:** 麦克风检测、HTTP 检测、Live2D 动作、错误处理 - -## 测试文件 - -- `js/core/audio/recorder.test.browser.js` - 浏览器兼容的麦克风可用性检测和 HTTP 非本地访问检测测试 -- `js/core/mcp/tools.test.browser.js` - 浏览器兼容的 MCP 工具和 Live2D 动作执行测试 - -**注意:** `.browser.js` 版本无需任何 npm 依赖。它们使用内置在 `test-runner.html` 中的简单测试框架。 - -## 运行测试 - -### 基于浏览器的测试运行器(无需 npm!) - -1. 启动本地服务器: -```bash -cd main/xiaozhi-server/test -python -m http.server 8007 -``` - -2. 在浏览器中打开 `http://localhost:8007/test-runner.html` - -3. 点击 "▶ Run All Tests"(运行所有测试)按钮 - -就这么简单!无需 npm,无需 package.json,无需任何依赖。 - -## 测试覆盖 - -### `recorder.test.browser.js` -- ✅ `checkMicrophoneAvailability()` - 当麦克风可用时返回 true -- ✅ `checkMicrophoneAvailability()` - 当麦克风不可用时返回 false -- ✅ `checkMicrophoneAvailability()` - 当浏览器不支持 getUserMedia 时返回 false -- ✅ `isHttpNonLocalhost()` - 对于 HTTP 非本地访问返回 true -- ✅ `isHttpNonLocalhost()` - 对于 localhost 返回 false -- ✅ `isHttpNonLocalhost()` - 对于 127.0.0.1 返回 false -- ✅ `isHttpNonLocalhost()` - 对于私有 IP 地址返回 false -- ✅ `isHttpNonLocalhost()` - 对于 HTTPS 协议返回 false - -### `tools.test.browser.js` -- ✅ `executeMcpTool('live2d.smile')` - 执行 FlickUp 动作 -- ✅ `executeMcpTool('live2d.wave')` - 执行 Tap 动作 -- ✅ `executeMcpTool('live2d.action')` - 执行自定义动作 -- ✅ `executeMcpTool()` - 优雅处理缺失的 Live2D 管理器 -- ✅ `executeMcpTool()` - 优雅处理未知工具 - -## 编写新测试 - -添加新功能时,创建一个遵循以下模式的 `.browser.js` 测试文件: - -```javascript -// your-module.test.browser.js -import { yourFunction } from './your-module.js'; - -describe('您的功能', () => { - beforeEach(() => { - // 设置模拟并重置状态 - vi.clearAllMocks(); - }); - - test('应该执行某些操作', () => { - // 准备 - const input = 'test'; - - // 执行 - const result = yourFunction(input); - - // 断言 - expect(result).toBe('expected'); - }); -}); -``` - -## 模拟指南 - -- 使用 `vi.fn()` 创建函数模拟 -- 使用 `vi.fn().mockResolvedValue(value)` 创建解析的异步模拟 -- 使用 `vi.fn().mockRejectedValue(error)` 创建拒绝的异步模拟 -- 在 `beforeEach` 中使用 `vi.clearAllMocks()` 重置状态 -- 模拟浏览器 API(`navigator`、`window.location`、`localStorage`、`fetch`) -- 需要时模拟 DOM 元素(`document.getElementById` 等) - -## 可用的测试函数 - -浏览器测试框架提供: -- `describe(name, fn)` - 定义测试套件 -- `test(name, fn)` - 定义测试用例 -- `beforeEach(fn)` - 在每个测试之前运行 -- `afterEach(fn)` - 在每个测试之后运行 -- `expect(actual)` - 断言对象,包含: - - `.toBe(expected)` - 严格相等 - - `.toHaveBeenCalled()` - 函数已被调用 - - `.toHaveBeenCalledWith(...args)` - 函数已使用特定参数调用 - - `.toContain(substring)` - 字符串包含子字符串 -- `vi.fn(impl?)` - 创建模拟函数 -- `vi.clearAllMocks()` - 清除所有模拟 - -## 注意事项 - -- 测试使用 ES 模块(`import`/`export`) -- 测试直接在浏览器中运行(无需 Node.js) -- 无需 npm 依赖 - 所有内容都是自包含的 -- 测试运行器(`test-runner.html`)包含一个简单的测试框架 -- 点击 "Run All Tests"(运行所有测试)时自动加载测试 diff --git a/main/xiaozhi-server/test/js/core/audio/recorder.test.browser.js b/main/xiaozhi-server/test/js/core/audio/recorder.test.browser.js index e7cf9cd3..6d88b5f1 100644 --- a/main/xiaozhi-server/test/js/core/audio/recorder.test.browser.js +++ b/main/xiaozhi-server/test/js/core/audio/recorder.test.browser.js @@ -1,154 +1,72 @@ -/** - * Audio recording module tests - Browser compatible version - * Test microphone availability detection functionality - * - * This version works without Vitest - uses the simple test framework from test-runner.html - */ - -import { checkMicrophoneAvailability, isHttpNonLocalhost } from './recorder.js'; - -describe('Microphone Availability Detection', () => { - beforeEach(() => { - // Reset mocks before each test - vi.clearAllMocks(); - }); - - /** - * Test checkMicrophoneAvailability function - success case - */ - test('should return true when microphone is available', async () => { - // Mock navigator.mediaDevices.getUserMedia to return a successful stream - const mockTrack = { - stop: vi.fn() - }; - const mockStream = { - getTracks: () => [mockTrack] - }; - - navigator.mediaDevices.getUserMedia = vi.fn().mockResolvedValue(mockStream); - - const result = await checkMicrophoneAvailability(); - - expect(result).toBe(true); - expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ - audio: { - echoCancellation: true, - noiseSuppression: true, - sampleRate: 16000, - channelCount: 1 - } - }); - expect(mockTrack.stop).toHaveBeenCalled(); - }); - - /** - * Test checkMicrophoneAvailability function - failure case - */ - test('should return false when microphone is not available', async () => { - // Mock getUserMedia to throw an error - const mockError = new Error('Permission denied'); - navigator.mediaDevices.getUserMedia = vi.fn().mockRejectedValue(mockError); - - const result = await checkMicrophoneAvailability(); - - expect(result).toBe(false); - expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalled(); - }); - - /** - * Test checkMicrophoneAvailability function - browser not supported - */ - test('should return false when browser does not support getUserMedia', async () => { - // Mock navigator.mediaDevices.getUserMedia to be undefined - const originalGetUserMedia = navigator.mediaDevices.getUserMedia; - navigator.mediaDevices.getUserMedia = undefined; - - const result = await checkMicrophoneAvailability(); - - expect(result).toBe(false); - - // Restore - navigator.mediaDevices.getUserMedia = originalGetUserMedia; - }); - - /** - * Test isHttpNonLocalhost function - HTTP non-localhost - * Note: window.location properties are read-only in browsers, so we test the logic indirectly - */ - test('should return true for HTTP non-localhost access', () => { - // Since window.location is read-only, we'll test by checking the actual implementation - // This test verifies the function works correctly with the current location - // In a real browser environment, this would test against actual location - const result = isHttpNonLocalhost(); - // Just verify the function runs without error - expect(typeof result).toBe('boolean'); - }); - - /** - * Test isHttpNonLocalhost function - localhost should return false - * Note: window.location properties are read-only in browsers - */ - test('should return false for localhost', () => { - // Test the logic by checking if current location is localhost - const result = isHttpNonLocalhost(); - // If we're on localhost, result should be false - if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { - expect(result).toBe(false); - } else { - // Otherwise just verify function returns boolean - expect(typeof result).toBe('boolean'); - } - }); - - /** - * Test isHttpNonLocalhost function - 127.0.0.1 should return false - * Note: window.location properties are read-only in browsers - */ - test('should return false for 127.0.0.1', () => { - // Test the logic by checking if current location is 127.0.0.1 - const result = isHttpNonLocalhost(); - // If we're on 127.0.0.1, result should be false - if (window.location.hostname === '127.0.0.1') { - expect(result).toBe(false); - } else { - // Otherwise just verify function returns boolean - expect(typeof result).toBe('boolean'); - } - }); - - /** - * Test isHttpNonLocalhost function - private IP should return false - * Note: window.location properties are read-only in browsers - */ - test('should return false for private IP addresses', () => { - // Test the logic by checking if current location is a private IP - const result = isHttpNonLocalhost(); - const hostname = window.location.hostname; - const isPrivateIP = hostname.startsWith('192.168.') || - hostname.startsWith('10.') || - hostname.startsWith('172.'); - - if (isPrivateIP && window.location.protocol === 'http:') { - expect(result).toBe(false); - } else { - // Otherwise just verify function returns boolean - expect(typeof result).toBe('boolean'); - } - }); - - /** - * Test isHttpNonLocalhost function - HTTPS should return false - * Note: window.location properties are read-only in browsers - */ - test('should return false for HTTPS protocol', () => { - // Test the logic by checking if current protocol is HTTPS - const result = isHttpNonLocalhost(); - // If we're on HTTPS, result should be false - if (window.location.protocol === 'https:') { - expect(result).toBe(false); - } else { - // Otherwise just verify function returns boolean - expect(typeof result).toBe('boolean'); - } - }); -}); +import { checkMicrophoneAvailability, isHttpNonLocalhost } from './recorder.js'; + +describe('Microphone Availability Detection', () => { + beforeEach(() => vi.clearAllMocks()); + + test('should return true when microphone is available', async () => { + const mockTrack = { stop: vi.fn() }; + const mockStream = { getTracks: () => [mockTrack] }; + navigator.mediaDevices.getUserMedia = vi.fn().mockResolvedValue(mockStream); + const result = await checkMicrophoneAvailability(); + expect(result).toBe(true); + expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 16000, channelCount: 1 } }); + expect(mockTrack.stop).toHaveBeenCalled(); + }); + + test('should return false when microphone is not available', async () => { + navigator.mediaDevices.getUserMedia = vi.fn().mockRejectedValue(new Error('Permission denied')); + const result = await checkMicrophoneAvailability(); + expect(result).toBe(false); + expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalled(); + }); + + test('should return false when browser does not support getUserMedia', async () => { + const originalGetUserMedia = navigator.mediaDevices.getUserMedia; + navigator.mediaDevices.getUserMedia = undefined; + const result = await checkMicrophoneAvailability(); + expect(result).toBe(false); + navigator.mediaDevices.getUserMedia = originalGetUserMedia; + }); + + test('should return true for HTTP non-localhost access', () => { + expect(typeof isHttpNonLocalhost()).toBe('boolean'); + }); + + test('should return false for localhost', () => { + const result = isHttpNonLocalhost(); + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + expect(result).toBe(false); + } else { + expect(typeof result).toBe('boolean'); + } + }); + + test('should return false for 127.0.0.1', () => { + const result = isHttpNonLocalhost(); + if (window.location.hostname === '127.0.0.1') { + expect(result).toBe(false); + } else { + expect(typeof result).toBe('boolean'); + } + }); + + test('should return false for private IP addresses', () => { + const result = isHttpNonLocalhost(); + const hostname = window.location.hostname; + const isPrivateIP = hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.'); + if (isPrivateIP && window.location.protocol === 'http:') { + expect(result).toBe(false); + } else { + expect(typeof result).toBe('boolean'); + } + }); + + test('should return false for HTTPS protocol', () => { + const result = isHttpNonLocalhost(); + if (window.location.protocol === 'https:') { + expect(result).toBe(false); + } else { + expect(typeof result).toBe('boolean'); + } + }); +}); \ No newline at end of file diff --git a/main/xiaozhi-server/test/js/core/mcp/tools.js b/main/xiaozhi-server/test/js/core/mcp/tools.js index 3aab88ae..3fca3001 100644 --- a/main/xiaozhi-server/test/js/core/mcp/tools.js +++ b/main/xiaozhi-server/test/js/core/mcp/tools.js @@ -40,7 +40,7 @@ export async function initMcpTools() { renderMcpTools(); // Only setup event listeners if DOM elements exist if (document.getElementById('toggleMcpTools')) { - setupMcpEventListeners(); + setupMcpEventListeners(); } } diff --git a/main/xiaozhi-server/test/js/core/mcp/tools.test.browser.js b/main/xiaozhi-server/test/js/core/mcp/tools.test.browser.js index 15d0f3a9..dcbd4a41 100644 --- a/main/xiaozhi-server/test/js/core/mcp/tools.test.browser.js +++ b/main/xiaozhi-server/test/js/core/mcp/tools.test.browser.js @@ -1,155 +1,64 @@ -/** - * 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('未知工具'); - }); -}); +import { executeMcpTool, initMcpTools } from './tools.js'; + +describe('Live2D Action Tools', () => { + let mockLive2DManager, originalChatApp; + + beforeEach(() => { + vi.clearAllMocks(); + originalChatApp = window.chatApp; + mockLive2DManager = { motion: vi.fn() }; + window.chatApp = { live2dManager: mockLive2DManager }; + localStorage.getItem = vi.fn(() => null); + 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'] } }]) })); + const mockContainer = { innerHTML: '', appendChild: vi.fn(), textContent: '' }; + document.getElementById = vi.fn((id) => { + if (id === 'mcpToolsContainer' || id === 'mcpPropertiesContainer') return mockContainer; + if (id === 'mcpToolsCount') return { textContent: '' }; + return null; + }); + }); + + afterEach(() => { window.chatApp = originalChatApp; }); + + 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'); + }); + + 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'); + }); + + 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'); + }); + + 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管理器未初始化'); + }); + + test('should handle unknown tool gracefully', async () => { + await initMcpTools(); + const result = executeMcpTool('unknown.tool', {}); + expect(result.success).toBe(false); + expect(result.error).toContain('未知工具'); + }); +}); \ No newline at end of file diff --git a/main/xiaozhi-server/test/test-runner.html b/main/xiaozhi-server/test/test-runner.html index 17a81129..b8b1b92e 100644 --- a/main/xiaozhi-server/test/test-runner.html +++ b/main/xiaozhi-server/test/test-runner.html @@ -1,617 +1,59 @@ - - - - - - Unit Tests Runner - - - -
-
-

🧪 Unit Tests Runner

-

Browser-based test runner for xiaozhi test modules

-
- -
- - -
-
-
Total
-
0
-
-
-
Passed
-
0
-
-
-
Failed
-
0
-
-
-
- -
- -
- Click "Run All Tests" to start testing -
-
- -
-
- - - - + + + + + + Unit Tests Runner + + + +
+
+

🧪 Unit Tests Runner

+

Browser-based test runner for xiaozhi test modules

+
+
+ + +
+
Total
0
+
Passed
0
+
Failed
0
+
+
+
+ +
Click "Run All Tests" to start testing
+
+
+
+ + + \ No newline at end of file