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",
);
});
});
});