Files
kandev/apps/web/lib/user-settings-sync.test.ts
T

105 lines
3.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { waitFor } from "@testing-library/react";
import { ApiError } from "@/lib/api/client";
import { updateUserSettings } from "@/lib/api/domains/settings-api";
import {
createQueuedUserSettingsSync,
createQueuedUserSettingsSyncWithResponse,
} from "./user-settings-sync";
vi.mock("@/lib/api/domains/settings-api", () => ({
updateUserSettings: vi.fn(),
}));
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((res) => {
resolve = res;
});
return { promise, resolve };
}
describe("createQueuedUserSettingsSync", () => {
beforeEach(() => {
window.localStorage.clear();
vi.mocked(updateUserSettings).mockReset();
});
it("serializes backend writes so later payloads are sent after earlier ones finish", async () => {
const first = deferred<Awaited<ReturnType<typeof updateUserSettings>>>();
vi.mocked(updateUserSettings)
.mockReturnValueOnce(first.promise)
.mockResolvedValueOnce({ settings: {} } as Awaited<ReturnType<typeof updateUserSettings>>);
const sync = createQueuedUserSettingsSync<string>((value) => ({
preferred_shell: value,
}));
void sync("bash");
void sync("zsh");
await waitFor(() => {
expect(updateUserSettings).toHaveBeenCalledTimes(1);
});
expect(updateUserSettings).toHaveBeenCalledWith({ preferred_shell: "bash" });
first.resolve({ settings: {} } as Awaited<ReturnType<typeof updateUserSettings>>);
await waitFor(() => {
expect(updateUserSettings).toHaveBeenCalledTimes(2);
expect(updateUserSettings).toHaveBeenLastCalledWith({ preferred_shell: "zsh" });
});
});
it("returns the authoritative response when the caller needs its revision", async () => {
const response = { settings: { revision: 7 } } as Awaited<
ReturnType<typeof updateUserSettings>
>;
vi.mocked(updateUserSettings).mockResolvedValueOnce(response);
const sync = createQueuedUserSettingsSyncWithResponse<string>((value) => ({
preferred_shell: value,
}));
await expect(sync("zsh")).resolves.toBe(response);
});
it("retries a transient settings write before processing the next payload", async () => {
vi.mocked(updateUserSettings)
.mockRejectedValueOnce(new Error("temporary failure"))
.mockResolvedValueOnce({ settings: {} } as Awaited<ReturnType<typeof updateUserSettings>>)
.mockResolvedValueOnce({ settings: {} } as Awaited<ReturnType<typeof updateUserSettings>>);
const sync = createQueuedUserSettingsSync<string>((value) => ({
preferred_shell: value,
}));
void sync("bash");
void sync("zsh");
await waitFor(() => {
expect(updateUserSettings).toHaveBeenNthCalledWith(1, { preferred_shell: "bash" });
expect(updateUserSettings).toHaveBeenNthCalledWith(2, { preferred_shell: "bash" });
expect(updateUserSettings).toHaveBeenNthCalledWith(3, { preferred_shell: "zsh" });
});
});
it("does not retry an API error", async () => {
vi.mocked(updateUserSettings).mockRejectedValueOnce(new ApiError("invalid settings", 400, {}));
const sync = createQueuedUserSettingsSync<string>((value) => ({
preferred_shell: value,
}));
await expect(sync("bash")).rejects.toBeInstanceOf(ApiError);
expect(updateUserSettings).toHaveBeenCalledTimes(1);
});
it("rejects failed writes so explicit-save callers remain dirty and retryable", async () => {
const failure = new Error("network unavailable");
vi.mocked(updateUserSettings).mockRejectedValue(failure);
const sync = createQueuedUserSettingsSync<string>((value) => ({
preferred_shell: value,
}));
await expect(sync("zsh")).rejects.toBe(failure);
});
});