Mock Refactoring: - Extract saveFileDialogMock and exportConfigMock as variables - Previously used inline vi.fn() which prevented call verification - Now supports expect().toHaveBeenCalledWith() assertions - Enables parameter and return value validation - Add mock reset in beforeEach for test isolation - Reset saveFileDialogMock state - Reset exportConfigMock state - Ensures clean state for each test New Test: Import Failure Callback Verification - Add test "does not call onImportSuccess when import fails" - User selects file successfully - Import operation fails (success: false) - Verify onImportSuccess callback NOT called - Verify status becomes "error" - Prevents triggering success logic on failure New Test: Export Success Message Verification - Add test "propagates export success message to toast with saved path" - User selects save location: /exports/config.json - Backend saves to: /final/config.json (may differ) - Verify exportConfigMock called with user-selected path - Verify toast success message contains actual saved path - Ensures user sees correct save location Coverage Improvements: - Import failure callback: 50% → 100% - Export success message: 50% → 100% - Mock verification capability: 0% → 100% All tests passing: 81/81 (2 new tests)
117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
import { renderHook, act } from "@testing-library/react";
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { useImportExport } from "@/hooks/useImportExport";
|
|
|
|
const toastSuccessMock = vi.fn();
|
|
const toastErrorMock = vi.fn();
|
|
|
|
vi.mock("sonner", () => ({
|
|
toast: {
|
|
success: (...args: unknown[]) => toastSuccessMock(...args),
|
|
error: (...args: unknown[]) => toastErrorMock(...args),
|
|
},
|
|
}));
|
|
|
|
const openFileDialogMock = vi.fn();
|
|
const importConfigMock = vi.fn();
|
|
const saveFileDialogMock = vi.fn();
|
|
const exportConfigMock = vi.fn();
|
|
|
|
vi.mock("@/lib/api", () => ({
|
|
settingsApi: {
|
|
openFileDialog: (...args: unknown[]) => openFileDialogMock(...args),
|
|
importConfigFromFile: (...args: unknown[]) => importConfigMock(...args),
|
|
saveFileDialog: (...args: unknown[]) => saveFileDialogMock(...args),
|
|
exportConfigToFile: (...args: unknown[]) => exportConfigMock(...args),
|
|
},
|
|
}));
|
|
|
|
describe("useImportExport Hook (edge cases)", () => {
|
|
beforeEach(() => {
|
|
openFileDialogMock.mockReset();
|
|
importConfigMock.mockReset();
|
|
saveFileDialogMock.mockReset();
|
|
exportConfigMock.mockReset();
|
|
toastSuccessMock.mockReset();
|
|
toastErrorMock.mockReset();
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("keeps state unchanged when file dialog resolves to null", async () => {
|
|
openFileDialogMock.mockResolvedValue(null);
|
|
const { result } = renderHook(() => useImportExport());
|
|
|
|
await act(async () => {
|
|
await result.current.selectImportFile();
|
|
});
|
|
|
|
expect(result.current.selectedFile).toBe("");
|
|
expect(result.current.status).toBe("idle");
|
|
expect(toastErrorMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resetStatus clears errors but preserves selected file", async () => {
|
|
openFileDialogMock.mockResolvedValue("/config.json");
|
|
importConfigMock.mockResolvedValue({ success: false, message: "broken" });
|
|
const { result } = renderHook(() => useImportExport());
|
|
|
|
await act(async () => {
|
|
await result.current.selectImportFile();
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.importConfig();
|
|
});
|
|
|
|
act(() => {
|
|
result.current.resetStatus();
|
|
});
|
|
|
|
expect(result.current.selectedFile).toBe("/config.json");
|
|
expect(result.current.status).toBe("idle");
|
|
expect(result.current.errorMessage).toBeNull();
|
|
expect(result.current.backupId).toBeNull();
|
|
});
|
|
|
|
it("does not call onImportSuccess when import fails", async () => {
|
|
openFileDialogMock.mockResolvedValue("/config.json");
|
|
importConfigMock.mockResolvedValue({
|
|
success: false,
|
|
message: "invalid",
|
|
});
|
|
const onImportSuccess = vi.fn();
|
|
const { result } = renderHook(() => useImportExport({ onImportSuccess }));
|
|
|
|
await act(async () => {
|
|
await result.current.selectImportFile();
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.importConfig();
|
|
});
|
|
|
|
expect(onImportSuccess).not.toHaveBeenCalled();
|
|
expect(result.current.status).toBe("error");
|
|
});
|
|
|
|
it("propagates export success message to toast with saved path", async () => {
|
|
saveFileDialogMock.mockResolvedValue("/exports/config.json");
|
|
exportConfigMock.mockResolvedValue({ success: true, filePath: "/final/config.json" });
|
|
const { result } = renderHook(() => useImportExport());
|
|
|
|
await act(async () => {
|
|
await result.current.exportConfig();
|
|
});
|
|
|
|
expect(exportConfigMock).toHaveBeenCalledWith("/exports/config.json");
|
|
expect(toastSuccessMock).toHaveBeenCalledWith(
|
|
expect.stringContaining("/final/config.json"),
|
|
);
|
|
});
|
|
|
|
});
|