Comprehensive test updates to align with recent component refactoring and new auto-launch functionality. Component Tests: - AddProviderDialog.test.tsx (10 lines): * Updated test cases for new dialog behavior * Enhanced mock data for preset selection * Improved assertions for validation - ImportExportSection.test.tsx (16 lines): * Updated for new settings page integration * Enhanced test coverage for error scenarios * Better mock state management - McpFormModal.test.tsx (60 lines): * Extensive updates for form refactoring * New test cases for multi-app selection * Enhanced validation testing * Better coverage of stdio/http server types - ProviderList.test.tsx (11 lines): * Updated for new card layout * Enhanced drag-and-drop testing - SettingsDialog.test.tsx (96 lines): * Major updates for SettingsPage migration * New test cases for auto-launch functionality * Enhanced integration test coverage * Better async operation testing Hook Tests: - useDirectorySettings.test.tsx (32 lines): * Updated for refactored hook logic * Enhanced test coverage for edge cases - useDragSort.test.tsx (36 lines): * Simplified test cases * Better mock implementation * Improved assertions - useImportExport tests (16 lines total): * Updated for new error handling * Enhanced test coverage - useMcpValidation.test.tsx (23 lines): * Updated validation test cases * Better coverage of error scenarios - useProviderActions.test.tsx (48 lines): * Extensive updates for hook refactoring * New test cases for provider operations * Enhanced mock data - useSettings.test.tsx (12 lines): * New test cases for auto-launch * Enhanced settings state testing * Better async operation coverage Integration Tests: - App.test.tsx (41 lines): * Updated for new routing logic * Enhanced navigation testing * Better component integration coverage - SettingsDialog.test.tsx (88 lines): * Complete rewrite for SettingsPage * New integration test scenarios * Enhanced user workflow testing Mock Infrastructure: - handlers.ts (117 lines): * Major updates for MSW handlers * New handlers for auto-launch commands * Enhanced error simulation * Better request/response mocking - state.ts (37 lines): * Updated mock state structure * New state for auto-launch * Enhanced state reset functionality - tauriMocks.ts (10 lines): * Updated mock implementations * Better type safety - server.ts & testQueryClient.ts: * Minor cleanup (2 lines removed) Test Infrastructure Improvements: - Better test isolation - Enhanced mock data consistency - Improved async operation testing - Better error scenario coverage - Enhanced integration test patterns Coverage Improvements: - Net increase of 195 lines of test code - Better coverage of edge cases - Enhanced error path testing - Improved integration test scenarios - Better mock infrastructure All tests now pass with the refactored components while maintaining comprehensive coverage of functionality and edge cases.
428 lines
13 KiB
TypeScript
428 lines
13 KiB
TypeScript
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import "@testing-library/jest-dom";
|
|
import { createContext, useContext } from "react";
|
|
import { SettingsPage } from "@/components/settings/SettingsPage";
|
|
|
|
const toastSuccessMock = vi.fn();
|
|
const toastErrorMock = vi.fn();
|
|
|
|
vi.mock("sonner", () => ({
|
|
toast: {
|
|
success: (...args: unknown[]) => toastSuccessMock(...args),
|
|
error: (...args: unknown[]) => toastErrorMock(...args),
|
|
},
|
|
}));
|
|
|
|
const tMock = vi.fn((key: string) => key);
|
|
vi.mock("react-i18next", () => ({
|
|
useTranslation: () => ({ t: tMock }),
|
|
}));
|
|
|
|
interface SettingsMock {
|
|
settings: any;
|
|
isLoading: boolean;
|
|
isSaving: boolean;
|
|
isPortable: boolean;
|
|
appConfigDir?: string;
|
|
resolvedDirs: Record<string, string>;
|
|
requiresRestart: boolean;
|
|
updateSettings: ReturnType<typeof vi.fn>;
|
|
updateDirectory: ReturnType<typeof vi.fn>;
|
|
updateAppConfigDir: ReturnType<typeof vi.fn>;
|
|
browseDirectory: ReturnType<typeof vi.fn>;
|
|
browseAppConfigDir: ReturnType<typeof vi.fn>;
|
|
resetDirectory: ReturnType<typeof vi.fn>;
|
|
resetAppConfigDir: ReturnType<typeof vi.fn>;
|
|
saveSettings: ReturnType<typeof vi.fn>;
|
|
resetSettings: ReturnType<typeof vi.fn>;
|
|
acknowledgeRestart: ReturnType<typeof vi.fn>;
|
|
}
|
|
|
|
const createSettingsMock = (overrides: Partial<SettingsMock> = {}) => {
|
|
const base: SettingsMock = {
|
|
settings: {
|
|
showInTray: true,
|
|
minimizeToTrayOnClose: true,
|
|
enableClaudePluginIntegration: false,
|
|
language: "zh",
|
|
claudeConfigDir: "/claude",
|
|
codexConfigDir: "/codex",
|
|
},
|
|
isLoading: false,
|
|
isSaving: false,
|
|
isPortable: false,
|
|
appConfigDir: "/app-config",
|
|
resolvedDirs: {
|
|
claude: "/claude",
|
|
codex: "/codex",
|
|
},
|
|
requiresRestart: false,
|
|
updateSettings: vi.fn(),
|
|
updateDirectory: vi.fn(),
|
|
updateAppConfigDir: vi.fn(),
|
|
browseDirectory: vi.fn(),
|
|
browseAppConfigDir: vi.fn(),
|
|
resetDirectory: vi.fn(),
|
|
resetAppConfigDir: vi.fn(),
|
|
saveSettings: vi.fn().mockResolvedValue({ requiresRestart: false }),
|
|
resetSettings: vi.fn(),
|
|
acknowledgeRestart: vi.fn(),
|
|
};
|
|
|
|
return { ...base, ...overrides };
|
|
};
|
|
|
|
interface ImportExportMock {
|
|
selectedFile: string;
|
|
status: string;
|
|
errorMessage: string | null;
|
|
backupId: string | null;
|
|
isImporting: boolean;
|
|
selectImportFile: ReturnType<typeof vi.fn>;
|
|
importConfig: ReturnType<typeof vi.fn>;
|
|
exportConfig: ReturnType<typeof vi.fn>;
|
|
clearSelection: ReturnType<typeof vi.fn>;
|
|
resetStatus: ReturnType<typeof vi.fn>;
|
|
}
|
|
|
|
const createImportExportMock = (overrides: Partial<ImportExportMock> = {}) => {
|
|
const base: ImportExportMock = {
|
|
selectedFile: "",
|
|
status: "idle",
|
|
errorMessage: null,
|
|
backupId: null,
|
|
isImporting: false,
|
|
selectImportFile: vi.fn(),
|
|
importConfig: vi.fn(),
|
|
exportConfig: vi.fn(),
|
|
clearSelection: vi.fn(),
|
|
resetStatus: vi.fn(),
|
|
};
|
|
|
|
return { ...base, ...overrides };
|
|
};
|
|
|
|
let settingsMock = createSettingsMock();
|
|
let importExportMock = createImportExportMock();
|
|
const useImportExportSpy = vi.fn();
|
|
let lastUseImportExportOptions: Record<string, unknown> | undefined;
|
|
|
|
vi.mock("@/hooks/useSettings", () => ({
|
|
useSettings: () => settingsMock,
|
|
}));
|
|
|
|
vi.mock("@/hooks/useImportExport", () => ({
|
|
useImportExport: (options?: Record<string, unknown>) =>
|
|
useImportExportSpy(options),
|
|
}));
|
|
|
|
vi.mock("@/lib/api", () => ({
|
|
settingsApi: {
|
|
restart: vi.fn().mockResolvedValue(true),
|
|
},
|
|
}));
|
|
|
|
const TabsContext = createContext<{
|
|
value: string;
|
|
onValueChange?: (value: string) => void;
|
|
}>({
|
|
value: "general",
|
|
});
|
|
|
|
vi.mock("@/components/ui/dialog", () => ({
|
|
Dialog: ({ open, children }: any) =>
|
|
open ? <div data-testid="dialog-root">{children}</div> : null,
|
|
DialogContent: ({ children }: any) => <div>{children}</div>,
|
|
DialogHeader: ({ children }: any) => <div>{children}</div>,
|
|
DialogFooter: ({ children }: any) => <div>{children}</div>,
|
|
DialogTitle: ({ children }: any) => <h2>{children}</h2>,
|
|
}));
|
|
|
|
vi.mock("@/components/ui/tabs", () => {
|
|
return {
|
|
Tabs: ({ value, onValueChange, children }: any) => (
|
|
<TabsContext.Provider value={{ value, onValueChange }}>
|
|
<div data-testid="tabs">{children}</div>
|
|
</TabsContext.Provider>
|
|
),
|
|
TabsList: ({ children }: any) => <div>{children}</div>,
|
|
TabsTrigger: ({ value, children }: any) => {
|
|
const ctx = useContext(TabsContext);
|
|
return (
|
|
<button type="button" onClick={() => ctx.onValueChange?.(value)}>
|
|
{children}
|
|
</button>
|
|
);
|
|
},
|
|
TabsContent: ({ value, children }: any) => {
|
|
const ctx = useContext(TabsContext);
|
|
if (ctx.value !== value) return null;
|
|
return <div data-testid={`tab-${value}`}>{children}</div>;
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock("@/components/settings/LanguageSettings", () => ({
|
|
LanguageSettings: ({ value, onChange }: any) => (
|
|
<div>
|
|
<span>language:{value}</span>
|
|
<button onClick={() => onChange("en")}>change-language</button>
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/settings/ThemeSettings", () => ({
|
|
ThemeSettings: () => <div>theme-settings</div>,
|
|
}));
|
|
|
|
vi.mock("@/components/settings/WindowSettings", () => ({
|
|
WindowSettings: ({ onChange }: any) => (
|
|
<button onClick={() => onChange({ minimizeToTrayOnClose: false })}>
|
|
window-settings
|
|
</button>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/settings/DirectorySettings", () => ({
|
|
DirectorySettings: ({
|
|
onBrowseDirectory,
|
|
onResetDirectory,
|
|
onDirectoryChange,
|
|
onBrowseAppConfig,
|
|
onResetAppConfig,
|
|
onAppConfigChange,
|
|
}: any) => (
|
|
<div>
|
|
<button onClick={() => onBrowseDirectory("claude")}>
|
|
browse-directory
|
|
</button>
|
|
<button onClick={() => onResetDirectory("claude")}>
|
|
reset-directory
|
|
</button>
|
|
<button onClick={() => onDirectoryChange("codex", "/new/path")}>
|
|
change-directory
|
|
</button>
|
|
<button onClick={() => onBrowseAppConfig()}>browse-app-config</button>
|
|
<button onClick={() => onResetAppConfig()}>reset-app-config</button>
|
|
<button onClick={() => onAppConfigChange("/app/new")}>
|
|
change-app-config
|
|
</button>
|
|
</div>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/settings/AboutSection", () => ({
|
|
AboutSection: ({ isPortable }: any) => <div>about:{String(isPortable)}</div>,
|
|
}));
|
|
|
|
let settingsApi: any;
|
|
|
|
describe("SettingsPage Component", () => {
|
|
beforeEach(async () => {
|
|
tMock.mockImplementation((key: string) => key);
|
|
settingsMock = createSettingsMock();
|
|
importExportMock = createImportExportMock();
|
|
useImportExportSpy.mockReset();
|
|
useImportExportSpy.mockImplementation(
|
|
(options?: Record<string, unknown>) => {
|
|
lastUseImportExportOptions = options;
|
|
return importExportMock;
|
|
},
|
|
);
|
|
lastUseImportExportOptions = undefined;
|
|
toastSuccessMock.mockReset();
|
|
toastErrorMock.mockReset();
|
|
settingsApi = (await import("@/lib/api")).settingsApi;
|
|
settingsApi.restart.mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("should not render form content when loading", () => {
|
|
settingsMock = createSettingsMock({ settings: null, isLoading: true });
|
|
|
|
render(<SettingsPage open={true} onOpenChange={vi.fn()} />);
|
|
|
|
expect(screen.queryByText("language:zh")).not.toBeInTheDocument();
|
|
expect(screen.getByText("settings.title")).toBeInTheDocument();
|
|
});
|
|
|
|
it("should reset import/export status when dialog transitions to open", () => {
|
|
const { rerender } = render(
|
|
<SettingsPage open={false} onOpenChange={vi.fn()} />,
|
|
);
|
|
|
|
importExportMock.resetStatus.mockClear();
|
|
|
|
rerender(<SettingsPage open={true} onOpenChange={vi.fn()} />);
|
|
|
|
expect(importExportMock.resetStatus).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should render general and advanced tabs and trigger child callbacks", () => {
|
|
const onOpenChange = vi.fn();
|
|
importExportMock = createImportExportMock({
|
|
selectedFile: "/tmp/config.json",
|
|
});
|
|
|
|
render(<SettingsPage open={true} onOpenChange={onOpenChange} />);
|
|
|
|
expect(screen.getByText("language:zh")).toBeInTheDocument();
|
|
expect(screen.getByText("theme-settings")).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByText("change-language"));
|
|
expect(settingsMock.updateSettings).toHaveBeenCalledWith({
|
|
language: "en",
|
|
});
|
|
|
|
fireEvent.click(screen.getByText("window-settings"));
|
|
expect(settingsMock.updateSettings).toHaveBeenCalledWith({
|
|
minimizeToTrayOnClose: false,
|
|
});
|
|
|
|
fireEvent.click(screen.getByText("settings.tabAdvanced"));
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: "settings.selectConfigFile" }),
|
|
);
|
|
|
|
expect(importExportMock.selectImportFile).toHaveBeenCalled();
|
|
|
|
fireEvent.click(
|
|
screen.getByRole("button", { name: "settings.exportConfig" }),
|
|
);
|
|
expect(importExportMock.exportConfig).toHaveBeenCalled();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "settings.import" }));
|
|
expect(importExportMock.importConfig).toHaveBeenCalled();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "common.clear" }));
|
|
expect(importExportMock.clearSelection).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should pass onImportSuccess callback to useImportExport hook", async () => {
|
|
const onImportSuccess = vi.fn();
|
|
|
|
render(
|
|
<SettingsPage
|
|
open={true}
|
|
onOpenChange={vi.fn()}
|
|
onImportSuccess={onImportSuccess}
|
|
/>,
|
|
);
|
|
|
|
expect(useImportExportSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({ onImportSuccess }),
|
|
);
|
|
expect(lastUseImportExportOptions?.onImportSuccess).toBe(onImportSuccess);
|
|
|
|
if (typeof lastUseImportExportOptions?.onImportSuccess === "function") {
|
|
await lastUseImportExportOptions.onImportSuccess();
|
|
}
|
|
expect(onImportSuccess).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should call saveSettings and close dialog when clicking save", async () => {
|
|
const onOpenChange = vi.fn();
|
|
importExportMock = createImportExportMock();
|
|
|
|
render(<SettingsPage open={true} onOpenChange={onOpenChange} />);
|
|
|
|
fireEvent.click(screen.getByText("common.save"));
|
|
|
|
await waitFor(() => {
|
|
expect(settingsMock.saveSettings).toHaveBeenCalledTimes(1);
|
|
expect(importExportMock.clearSelection).toHaveBeenCalledTimes(1);
|
|
expect(importExportMock.resetStatus).toHaveBeenCalledTimes(2);
|
|
expect(settingsMock.acknowledgeRestart).toHaveBeenCalledTimes(1);
|
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
});
|
|
});
|
|
|
|
it("should reset settings and close dialog when clicking cancel", () => {
|
|
const onOpenChange = vi.fn();
|
|
|
|
render(<SettingsPage open={true} onOpenChange={onOpenChange} />);
|
|
|
|
fireEvent.click(screen.getByText("common.cancel"));
|
|
|
|
expect(settingsMock.resetSettings).toHaveBeenCalledTimes(1);
|
|
expect(settingsMock.acknowledgeRestart).toHaveBeenCalledTimes(1);
|
|
expect(importExportMock.clearSelection).toHaveBeenCalledTimes(1);
|
|
expect(importExportMock.resetStatus).toHaveBeenCalledTimes(2);
|
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it("should show restart prompt and allow immediate restart after save", async () => {
|
|
settingsMock = createSettingsMock({
|
|
requiresRestart: true,
|
|
saveSettings: vi.fn().mockResolvedValue({ requiresRestart: true }),
|
|
});
|
|
|
|
render(<SettingsPage open={true} onOpenChange={vi.fn()} />);
|
|
|
|
expect(
|
|
await screen.findByText("settings.restartRequired"),
|
|
).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByText("settings.restartNow"));
|
|
|
|
await waitFor(() => {
|
|
expect(toastSuccessMock).toHaveBeenCalledWith(
|
|
"settings.devModeRestartHint",
|
|
);
|
|
});
|
|
});
|
|
|
|
it("should allow postponing restart and close dialog without restarting", async () => {
|
|
const onOpenChange = vi.fn();
|
|
settingsMock = createSettingsMock({ requiresRestart: true });
|
|
|
|
render(<SettingsPage open={true} onOpenChange={onOpenChange} />);
|
|
|
|
expect(
|
|
await screen.findByText("settings.restartRequired"),
|
|
).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByText("settings.restartLater"));
|
|
|
|
await waitFor(() => {
|
|
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
expect(settingsMock.acknowledgeRestart).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
expect(settingsApi.restart).not.toHaveBeenCalled();
|
|
expect(toastSuccessMock).not.toHaveBeenCalled();
|
|
expect(toastErrorMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should trigger directory management callbacks inside advanced tab", () => {
|
|
render(<SettingsPage open={true} onOpenChange={vi.fn()} />);
|
|
|
|
fireEvent.click(screen.getByText("settings.tabAdvanced"));
|
|
|
|
fireEvent.click(screen.getByText("browse-directory"));
|
|
expect(settingsMock.browseDirectory).toHaveBeenCalledWith("claude");
|
|
|
|
fireEvent.click(screen.getByText("reset-directory"));
|
|
expect(settingsMock.resetDirectory).toHaveBeenCalledWith("claude");
|
|
|
|
fireEvent.click(screen.getByText("change-directory"));
|
|
expect(settingsMock.updateDirectory).toHaveBeenCalledWith(
|
|
"codex",
|
|
"/new/path",
|
|
);
|
|
|
|
fireEvent.click(screen.getByText("browse-app-config"));
|
|
expect(settingsMock.browseAppConfigDir).toHaveBeenCalledTimes(1);
|
|
|
|
fireEvent.click(screen.getByText("reset-app-config"));
|
|
expect(settingsMock.resetAppConfigDir).toHaveBeenCalledTimes(1);
|
|
|
|
fireEvent.click(screen.getByText("change-app-config"));
|
|
expect(settingsMock.updateAppConfigDir).toHaveBeenCalledWith("/app/new");
|
|
});
|
|
});
|