test: extend MCP UI test coverage with wizard, TOML, and error handling

## McpFormModal Component Tests (+5 tests)

### Infrastructure Improvements
- Enhance McpWizardModal mock from null to functional mock with testable onApply callback
- Refactor renderForm helper to support custom onSave/onClose mock injection
- Add McpServer type import for type-safe test data

### New Test Cases
1. **Wizard Integration**: Verify wizard generates config and auto-fills ID + JSON fields
   - Click "Use Wizard" → Apply → Form fields populated with wizard-id and config
   - Uses act() wrapper for React 18 async state updates

2. **TOML Auto-extraction (Codex)**: Test TOML → JSON conversion with ID extraction
   - Parse `[mcp.servers.demo]` → auto-fill ID as "demo"
   - Verify server object correctly parsed from TOML format
   - Codex-specific feature for config.toml compatibility

3. **TOML Validation Error**: Test missing required field handling
   - TOML with type="stdio" but no command → block submit
   - Display localized error toast: mcp.error.idRequired (3s duration)

4. **Edit Mode Immutability**: Verify ID field disabled during edit
   - ID input has disabled attribute and keeps original value
   - Config updates applied while enabled state preserved
   - syncOtherSide defaults to false in edit mode

5. **Error Recovery**: Test save failure button state restoration
   - Inject failing onSave mock → trigger error
   - Verify toast error displays translated message
   - Submit button disabled state resets to false for retry

## useMcpActions Hook Tests (+2 tests)

### New API Mocks
- Add syncEnabledToClaude and syncEnabledToCodex mock functions

### New Test Cases
1. **Backend Error Message Mapping**: Map Chinese error to i18n key
   - Backend: "stdio 类型的 MCP 服务器必须包含 command 字段"
   - Frontend: mcp.error.commandRequired (6s toast duration)

2. **Cross-app Sync Logic**: Verify conditional sync behavior
   - claude → claude: setEnabled called, syncEnabledToClaude NOT called
   - Validates sync only occurs when crossing app types

## Minor Changes
- McpPanel.test.tsx: Add trailing newline (formatter compliance)

## Test Coverage
- Test files: 17 (unchanged)
- Total tests: 112 → 119 (+7, +6.3%)
- Execution time: 3.20s
- All 119 tests passing 
This commit is contained in:
Jason
2025-10-26 15:03:05 +08:00
parent c3f712bc18
commit 885dd94803
3 changed files with 203 additions and 18 deletions

View File

@@ -17,6 +17,8 @@ const getConfigMock = vi.fn();
const setEnabledMock = vi.fn();
const upsertServerInConfigMock = vi.fn();
const deleteServerInConfigMock = vi.fn();
const syncEnabledToClaudeMock = vi.fn();
const syncEnabledToCodexMock = vi.fn();
vi.mock("@/lib/api", () => ({
mcpApi: {
@@ -24,6 +26,8 @@ vi.mock("@/lib/api", () => ({
setEnabled: (...args: unknown[]) => setEnabledMock(...args),
upsertServerInConfig: (...args: unknown[]) => upsertServerInConfigMock(...args),
deleteServerInConfig: (...args: unknown[]) => deleteServerInConfigMock(...args),
syncEnabledToClaude: (...args: unknown[]) => syncEnabledToClaudeMock(...args),
syncEnabledToCodex: (...args: unknown[]) => syncEnabledToCodexMock(...args),
},
}));
@@ -60,6 +64,8 @@ describe("useMcpActions", () => {
setEnabledMock.mockReset();
upsertServerInConfigMock.mockReset();
deleteServerInConfigMock.mockReset();
syncEnabledToClaudeMock.mockReset();
syncEnabledToCodexMock.mockReset();
toastSuccessMock.mockReset();
toastErrorMock.mockReset();
@@ -239,4 +245,36 @@ describe("useMcpActions", () => {
expect(captured).toBe(failure);
expect(toastErrorMock).toHaveBeenCalledWith("delete failed", { duration: 6000 });
});
it("maps backend error message when save fails with known detail", async () => {
const serverInput = createServer({ id: "input-id" });
const backendError = { message: "stdio 类型的 MCP 服务器必须包含 command 字段" };
upsertServerInConfigMock.mockRejectedValueOnce(backendError);
const { result } = renderUseMcpActions();
await expect(async () =>
result.current.saveServer("server-1", serverInput),
).rejects.toEqual(backendError);
expect(toastErrorMock).toHaveBeenCalledWith("mcp.error.commandRequired", {
duration: 6000,
});
});
it("syncs enabled state to counterpart when appType is claude", async () => {
const server = createServer();
getConfigMock.mockResolvedValueOnce(mockConfigResponse({ [server.id]: server }));
const { result } = renderUseMcpActions();
await act(async () => {
await result.current.reload();
});
await act(async () => {
await result.current.toggleEnabled(server.id, true);
});
expect(setEnabledMock).toHaveBeenCalledWith("claude", server.id, true);
expect(syncEnabledToClaudeMock).not.toHaveBeenCalled();
});
});