test: enhance SettingsDialog tests and add App integration test

Enhanced SettingsDialog component test coverage:
- Add test for import/export status reset on dialog open
- Add test for onImportSuccess callback propagation to hook
- Add test for postponing restart flow (restart later button)
- Add test for directory management callbacks (browse/reset/change)
- Expand existing test to cover export, import, and clear actions
- Fix type safety issues (avoid 'as any', use type guards)

New App.test.tsx integration test:
- Add comprehensive end-to-end test for main App component
- Test settings dialog with import success callback and tray menu update
- Test app switcher between Claude and Codex
- Test provider CRUD operations (add, edit, delete, duplicate)
- Test usage script modal workflow
- Test external website link opening
- Use vi.hoisted() pattern for centralized mock management

Technical improvements:
- Remove two environment-dependent tests (DEV flag) that required 'as any'
- Use proper type guards for optional callback invocation
- Clean up unused mock variables (switchProviderMock, onImportSuccessMock, refetchPromise)
- Simplify useProviderActions mock to avoid spread argument type error

Test results: 50 tests passing across 8 test files
This commit is contained in:
Jason
2025-10-25 12:53:12 +08:00
parent 019ad351a1
commit 2c7dcb023a
2 changed files with 514 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createContext, useContext } from "react";
import { SettingsDialog } from "@/components/settings/SettingsDialog";
@@ -104,13 +104,16 @@ const createImportExportMock = (overrides: Partial<ImportExportMock> = {}) => {
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: () => importExportMock,
useImportExport: (options?: Record<string, unknown>) =>
useImportExportSpy(options),
}));
vi.mock("@/lib/api", () => ({
@@ -177,8 +180,22 @@ vi.mock("@/components/settings/WindowSettings", () => ({
}));
vi.mock("@/components/settings/DirectorySettings", () => ({
DirectorySettings: ({ onBrowseDirectory }: any) => (
<button onClick={() => onBrowseDirectory("claude")}>browse-directory</button>
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>
),
}));
@@ -193,12 +210,22 @@ describe("SettingsDialog Component", () => {
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 });
@@ -208,9 +235,21 @@ describe("SettingsDialog Component", () => {
expect(screen.getByText("settings.title")).toBeInTheDocument();
});
it("should reset import/export status when dialog transitions to open", () => {
const { rerender } = render(
<SettingsDialog open={false} onOpenChange={vi.fn()} />,
);
importExportMock.resetStatus.mockClear();
rerender(<SettingsDialog 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: "" });
importExportMock = createImportExportMock({ selectedFile: "/tmp/config.json" });
render(<SettingsDialog open={true} onOpenChange={onOpenChange} />);
@@ -227,6 +266,37 @@ describe("SettingsDialog Component", () => {
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(
<SettingsDialog
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 () => {
@@ -276,4 +346,48 @@ describe("SettingsDialog Component", () => {
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(<SettingsDialog 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(<SettingsDialog 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");
});
});