b9eb6ae058
* refactor(web): remove legacy settings migration * fix(web): hydrate portable settings from boot state * fix(web): preserve sidebar sync contracts * fix(web): retain portable settings filters * fix(web): retry sidebar settings writes * fix(web): retain sidebar boot settings * fix(websocket): avoid shutdown teardown leaks * fix(boot): normalize sidebar task preferences * fix(settings): preserve saved views during hydration * fix(jira): avoid duplicate saved-view syncs * fix(jira): preserve saved views on fetch failure * fix(jira): retry saved-view hydration failures * refactor(jira): simplify hydration request cleanup * fix(websocket): close late clients after hub shutdown
80 lines
2.8 KiB
TypeScript
80 lines
2.8 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 } 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("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 sync("bash");
|
|
|
|
expect(updateUserSettings).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|