import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import "@testing-library/jest-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { WebdavSyncSection } from "@/components/settings/WebdavSyncSection"; import type { WebDavSyncSettings } from "@/types"; const toastSuccessMock = vi.fn(); const toastErrorMock = vi.fn(); const toastWarningMock = vi.fn(); const toastInfoMock = vi.fn(); vi.mock("sonner", () => ({ toast: { success: (...args: unknown[]) => toastSuccessMock(...args), error: (...args: unknown[]) => toastErrorMock(...args), warning: (...args: unknown[]) => toastWarningMock(...args), info: (...args: unknown[]) => toastInfoMock(...args), }, })); vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key, }), })); vi.mock("@/components/ui/button", () => ({ Button: ({ children, ...props }: any) => , })); vi.mock("@/components/ui/input", () => ({ Input: (props: any) => , })); vi.mock("@/components/ui/select", () => ({ Select: ({ value, onValueChange, children }: any) => ( ), SelectTrigger: ({ children }: any) => <>{children}, SelectValue: () => null, SelectContent: ({ children }: any) => <>{children}, SelectItem: ({ value, children }: any) => ( ), })); vi.mock("@/components/ui/dialog", () => ({ Dialog: ({ open, children }: any) => (open ?
{children}
: null), DialogContent: ({ children }: any) =>
{children}
, DialogDescription: ({ children }: any) =>
{children}
, DialogFooter: ({ children }: any) =>
{children}
, DialogHeader: ({ children }: any) =>
{children}
, DialogTitle: ({ children }: any) =>

{children}

, })); const { settingsApiMock } = vi.hoisted(() => ({ settingsApiMock: { webdavTestConnection: vi.fn(), webdavSyncSaveSettings: vi.fn(), webdavSyncFetchRemoteInfo: vi.fn(), webdavSyncUpload: vi.fn(), webdavSyncDownload: vi.fn(), }, })); vi.mock("@/lib/api", () => ({ settingsApi: settingsApiMock, })); const baseConfig: WebDavSyncSettings = { enabled: true, baseUrl: "https://dav.example.com/dav/", username: "alice", password: "secret", remoteRoot: "cc-switch-sync", profile: "default", status: {}, }; function renderSection(config?: WebDavSyncSettings) { const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return render( , ); } describe("WebdavSyncSection", () => { beforeEach(() => { toastSuccessMock.mockReset(); toastErrorMock.mockReset(); toastWarningMock.mockReset(); toastInfoMock.mockReset(); settingsApiMock.webdavTestConnection.mockReset(); settingsApiMock.webdavSyncSaveSettings.mockReset(); settingsApiMock.webdavSyncFetchRemoteInfo.mockReset(); settingsApiMock.webdavSyncUpload.mockReset(); settingsApiMock.webdavSyncDownload.mockReset(); settingsApiMock.webdavSyncSaveSettings.mockResolvedValue({ success: true }); settingsApiMock.webdavTestConnection.mockResolvedValue({ success: true, message: "ok", }); settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValue({ deviceName: "My MacBook", createdAt: "2026-02-01T10:00:00Z", snapshotId: "snapshot-1", version: 2, compatible: true, artifacts: ["db.sql", "skills.zip"], }); settingsApiMock.webdavSyncUpload.mockResolvedValue({ status: "uploaded" }); settingsApiMock.webdavSyncDownload.mockResolvedValue({ status: "downloaded" }); }); it("shows validation error when saving without base url", async () => { renderSection({ ...baseConfig, baseUrl: "" }); fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" })); expect(toastErrorMock).toHaveBeenCalledWith("settings.webdavSync.missingUrl"); expect(settingsApiMock.webdavSyncSaveSettings).not.toHaveBeenCalled(); }); it("saves settings and auto tests connection", async () => { renderSection(baseConfig); fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" })); await waitFor(() => { expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledTimes(1); }); expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledWith( expect.objectContaining({ baseUrl: "https://dav.example.com/dav/", username: "alice", password: "secret", }), false, ); await waitFor(() => { expect(settingsApiMock.webdavTestConnection).toHaveBeenCalledWith( expect.objectContaining({ baseUrl: "https://dav.example.com/dav/", }), true, ); }); expect(toastSuccessMock).toHaveBeenCalledWith( "settings.webdavSync.saveAndTestSuccess", ); }); it("blocks upload when there are unsaved changes", async () => { renderSection(baseConfig); fireEvent.change( screen.getByPlaceholderText("settings.webdavSync.usernamePlaceholder"), { target: { value: "bob" } }, ); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.upload" }), ); expect(toastErrorMock).toHaveBeenCalledWith( "settings.webdavSync.unsavedChanges", ); expect(settingsApiMock.webdavSyncFetchRemoteInfo).not.toHaveBeenCalled(); }); it("disables sync buttons until config is saved", () => { renderSection(undefined); const uploadButton = screen.getByRole("button", { name: "settings.webdavSync.upload", }); const downloadButton = screen.getByRole("button", { name: "settings.webdavSync.download", }); expect(uploadButton).toBeDisabled(); expect(downloadButton).toBeDisabled(); expect(settingsApiMock.webdavSyncFetchRemoteInfo).not.toHaveBeenCalled(); }); it("fetches remote info and uploads after confirmation", async () => { renderSection(baseConfig); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.upload" }), ); await waitFor(() => { expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); }); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.confirmUpload.confirm", }), ); await waitFor(() => { expect(settingsApiMock.webdavSyncUpload).toHaveBeenCalledTimes(1); }); expect(toastSuccessMock).toHaveBeenCalledWith( "settings.webdavSync.uploadSuccess", ); }); it("blocks upload confirmation if form changes after dialog opens", async () => { renderSection(baseConfig); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.upload" }), ); await waitFor(() => { expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); }); fireEvent.change(screen.getByPlaceholderText("cc-switch-sync"), { target: { value: "new-root" }, }); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.confirmUpload.confirm", }), ); await waitFor(() => { expect(toastErrorMock).toHaveBeenCalledWith( "settings.webdavSync.unsavedChanges", ); }); expect(settingsApiMock.webdavSyncUpload).not.toHaveBeenCalled(); }); it("fetches remote info and downloads after confirmation", async () => { renderSection(baseConfig); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.download" }), ); await waitFor(() => { expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); }); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.confirmDownload.confirm", }), ); await waitFor(() => { expect(settingsApiMock.webdavSyncDownload).toHaveBeenCalledTimes(1); }); expect(toastSuccessMock).toHaveBeenCalledWith( "settings.webdavSync.downloadSuccess", ); }); it("blocks download confirmation if form changes after dialog opens", async () => { renderSection(baseConfig); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.download" }), ); await waitFor(() => { expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); }); fireEvent.change(screen.getByPlaceholderText("default"), { target: { value: "new-profile" }, }); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.confirmDownload.confirm", }), ); await waitFor(() => { expect(toastErrorMock).toHaveBeenCalledWith( "settings.webdavSync.unsavedChanges", ); }); expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled(); }); it("shows info when no remote snapshot is found for download", async () => { settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValueOnce({ empty: true }); renderSection(baseConfig); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.download" }), ); await waitFor(() => { expect(toastInfoMock).toHaveBeenCalledWith("settings.webdavSync.noRemoteData"); }); expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled(); }); it("blocks download when remote snapshot is incompatible", async () => { settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValueOnce({ deviceName: "Legacy Machine", createdAt: "2025-01-01T00:00:00Z", snapshotId: "legacy-snapshot", version: 1, compatible: false, artifacts: ["db.sql"], }); renderSection(baseConfig); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.download" }), ); await waitFor(() => { expect(toastErrorMock).toHaveBeenCalledWith( "settings.webdavSync.incompatibleVersion", ); }); expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled(); expect( screen.queryByRole("button", { name: "settings.webdavSync.confirmDownload.confirm", }), ).not.toBeInTheDocument(); }); it("shows error when download fails after confirmation", async () => { settingsApiMock.webdavSyncDownload.mockRejectedValueOnce(new Error("boom")); renderSection(baseConfig); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.download" }), ); await waitFor(() => { expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1); }); fireEvent.click( screen.getByRole("button", { name: "settings.webdavSync.confirmDownload.confirm", }), ); await waitFor(() => { expect(toastErrorMock).toHaveBeenCalledWith( "settings.webdavSync.downloadFailed", ); }); }); });