import { afterEach, expect, test, vi } from "vitest"; import { HttpResponse, http, server } from "~/test/msw/server"; import { cleanup, fireEvent, render, screen, userEvent } from "~/test/test-utils"; const navigateMock = vi.fn(async () => {}); vi.mock("@tanstack/react-router", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useNavigate: (() => navigateMock) as typeof actual.useNavigate, }; }); import { EditBackupPage } from "../edit-backup"; const repository = { shortId: "repo-1", name: "Repo 1", type: "local" }; const volume = { id: "volume-1", shortId: "vol-1", name: "Volume 1", config: { backend: "directory", path: "/mnt" }, }; const volumeFilesResponse = { files: [{ name: "project", path: "/project", type: "directory" }], path: "/", offset: 0, limit: 100, total: 1, hasMore: false, }; const renderEditBackupPage = ({ enabled, cronExpression }: { enabled: boolean; cronExpression: string }) => { const submittedBody = new Promise>((resolve) => { server.use( http.get("/api/v1/backups/:shortId", () => { return HttpResponse.json({ shortId: "backup-1", name: "Backup 1", enabled, repository, volume, cronExpression, retentionPolicy: null, includePaths: ["/project"], includePatterns: [], excludePatterns: [], excludeIfPresent: [], oneFileSystem: false, customResticParams: [], backupWebhooks: null, }); }), http.get("/api/v1/repositories", () => { return HttpResponse.json([repository]); }), http.get("/api/v1/volumes/:shortId/files", () => { return HttpResponse.json(volumeFilesResponse); }), http.patch("/api/v1/backups/:shortId", async ({ request }) => { const body = (await request.json()) as Record; resolve(body); return HttpResponse.json({ shortId: "backup-1", volume, repository, ...body, }); }), ); }); render(, { withSuspense: true }); return { submittedBody }; }; afterEach(() => { navigateMock.mockClear(); cleanup(); }); test("submits the computed cron expression when saving a daily schedule", async () => { const { submittedBody } = renderEditBackupPage({ enabled: true, cronExpression: "0 2 * * *" }); await userEvent.click(await screen.findByRole("button", { name: "Update schedule" })); await expect(submittedBody).resolves.toMatchObject({ frequency: "daily", enabled: true, cronExpression: "00 02 * * *", }); }); test("disables the schedule when switching an enabled custom cron schedule to manual only", async () => { const { submittedBody } = renderEditBackupPage({ enabled: true, cronExpression: "*/13 * * * *" }); const nativeFrequencySelect = (await screen.findAllByRole("combobox")) .at(1) ?.parentElement?.querySelector('select[aria-hidden="true"]'); if (!(nativeFrequencySelect instanceof HTMLSelectElement)) { throw new Error("Expected hidden native select for frequency field"); } fireEvent.change(nativeFrequencySelect, { target: { value: "manual" } }); await userEvent.click(screen.getByRole("button", { name: "Update schedule" })); await expect(submittedBody).resolves.toMatchObject({ frequency: "manual", enabled: false, cronExpression: "", }); }); test("preserves a disabled schedule when saving a non-manual frequency", async () => { const { submittedBody } = renderEditBackupPage({ enabled: false, cronExpression: "0 2 * * *" }); await userEvent.click(await screen.findByRole("button", { name: "Update schedule" })); await expect(submittedBody).resolves.toMatchObject({ frequency: "daily", enabled: false, cronExpression: "00 02 * * *", }); }); test("submits webhook headers and body as plain config values", async () => { const { submittedBody } = renderEditBackupPage({ enabled: true, cronExpression: "0 2 * * *" }); await userEvent.click(await screen.findByText("Advanced")); await userEvent.type(screen.getByLabelText("Pre-backup webhook"), "http://localhost:8080/stop"); fireEvent.change(screen.getByLabelText("Pre-backup webhook headers"), { target: { value: "Authorization: Bearer stop-token" }, }); fireEvent.change(screen.getByLabelText("Pre-backup webhook body"), { target: { value: '{"action":"stop"}' }, }); await userEvent.click(screen.getByRole("button", { name: "Update schedule" })); await expect(submittedBody).resolves.toMatchObject({ backupWebhooks: { pre: { url: "http://localhost:8080/stop", headers: ["Authorization: Bearer stop-token"], body: '{"action":"stop"}', }, post: null, }, }); });