Files
kandev/apps/web/hooks/use-ensure-user-settings.test.ts
T
José Almeida 9c16d337a2 feat(web): add global app status surface (#1869)
* feat(web): add app status bar

* feat(web): add app status bar visibility toggle

* feat(web): persist status bar item order

* fix(web): refine app status bar behavior

* fix(web): polish app status bar presentation

* test(web): split plugin slot coverage groups

* fix(web): address status surface review feedback

* fix(web): stabilize status bar overlay tests
2026-07-23 12:28:54 +01:00

304 lines
9.3 KiB
TypeScript

import { renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { UserSettingsState } from "@/lib/state/slices/settings/types";
type PendingTaskCreateLastUsed = {
repositoryId?: string;
branch?: string;
agentProfileId?: string;
executorProfileId?: string;
};
const mockFetchUserSettings = vi.fn();
const mockSetUserSettings = vi.fn((settings: UserSettingsState) => {
mockState.userSettings = settings;
});
const mockReadQueuedTaskCreateLastUsedState = vi.fn<() => PendingTaskCreateLastUsed>(() => ({
repositoryId: undefined,
branch: undefined,
agentProfileId: undefined,
executorProfileId: undefined,
}));
type MockState = {
userSettings: UserSettingsState;
setUserSettings: typeof mockSetUserSettings;
};
let mockState: MockState;
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: MockState) => unknown) => selector(mockState),
}));
vi.mock("@/lib/api/domains/settings-api", () => ({
fetchUserSettings: (...args: unknown[]) => mockFetchUserSettings(...args),
}));
vi.mock("@/components/task-create-dialog-handlers", () => ({
readQueuedTaskCreateLastUsedState: () => mockReadQueuedTaskCreateLastUsedState(),
}));
import {
__resetEnsureUserSettingsForTests,
useEnsureUserSettings,
} from "./use-ensure-user-settings";
function makeUnloadedSettings(): UserSettingsState {
return {
workspaceId: null,
workflowId: null,
kanbanViewMode: null,
repositoryIds: [],
tasksListSort: "updated_desc",
tasksListGroup: "state",
preferredShell: null,
shellOptions: [],
defaultEditorId: null,
enablePreviewOnClick: false,
chatSubmitKey: "cmd_enter",
reviewAutoMarkOnScroll: true,
confirmTaskArchive: true,
mcpTaskAgentProfileDefault: "current_task",
showReleaseNotification: true,
releaseNotesLastSeenVersion: null,
savedLayouts: [],
sidebarViews: [],
sidebarActiveViewId: null,
sidebarDraft: null,
sidebarTaskPrefs: { pinnedTaskIds: [], orderedTaskIds: [], subtaskOrderByParentId: {} },
taskCreateLastUsed: {
repositoryId: null,
branch: null,
agentProfileId: null,
executorProfileId: null,
},
jiraSavedViews: undefined,
jiraTaskPresets: undefined,
githubSavedPresets: undefined,
githubDefaultQueryPresets: undefined,
gitlabSavedPresets: undefined,
defaultUtilityAgentId: null,
keyboardShortcuts: {},
terminalLinkBehavior: "new_tab",
terminalFontFamily: null,
terminalFontSize: null,
changesPanelLayout: "tree",
systemMetricsDisplay: { showInTopbar: false },
appStatusBarOrder: { leftItemIds: [], rightItemIds: [] },
voiceMode: {
enabled: true,
engine: "auto",
language: "auto",
mode: "toggle",
autoSend: false,
whisperWebModel: "base",
},
lspAutoStartLanguages: [],
lspAutoInstallLanguages: [],
lspServerConfigs: {},
loaded: false,
};
}
function userSettingsResponse(taskCreateLastUsed = {}) {
return {
shell_options: [],
settings: {
task_create_last_used: taskCreateLastUsed,
},
};
}
beforeEach(() => {
__resetEnsureUserSettingsForTests();
vi.clearAllMocks();
mockState = {
userSettings: makeUnloadedSettings(),
setUserSettings: mockSetUserSettings,
};
});
describe("useEnsureUserSettings", () => {
it("fetches and stores user settings on first enabled mount", async () => {
mockFetchUserSettings.mockResolvedValue(
userSettingsResponse({ repository_id: "repo-1", branch: "main" }),
);
renderHook(() => useEnsureUserSettings(true));
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
expect(mockFetchUserSettings).toHaveBeenCalledWith({ cache: "no-store" });
expect(mockSetUserSettings.mock.calls[0]![0].loaded).toBe(true);
expect(mockSetUserSettings.mock.calls[0]![0].taskCreateLastUsed).toMatchObject({
repositoryId: "repo-1",
branch: "main",
});
});
it("joins an in-flight settings fetch instead of starting a duplicate request", async () => {
let resolveFetch: (value: unknown) => void = () => undefined;
mockFetchUserSettings.mockReturnValue(
new Promise((resolve) => {
resolveFetch = resolve;
}),
);
renderHook(() => useEnsureUserSettings(true));
renderHook(() => useEnsureUserSettings(true));
await waitFor(() => expect(mockFetchUserSettings).toHaveBeenCalledTimes(1));
resolveFetch(userSettingsResponse());
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
});
it("merges only defined queued task-create fields over fetched settings", async () => {
mockReadQueuedTaskCreateLastUsedState.mockReturnValue({
repositoryId: undefined,
branch: undefined,
agentProfileId: "agent-2",
executorProfileId: undefined,
});
mockFetchUserSettings.mockResolvedValue(
userSettingsResponse({
repository_id: "repo-1",
branch: "main",
agent_profile_id: "agent-1",
executor_profile_id: "exec-profile-1",
}),
);
renderHook(() => useEnsureUserSettings(true));
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
expect(mockSetUserSettings.mock.calls[0]![0].taskCreateLastUsed).toEqual({
repositoryId: "repo-1",
branch: "main",
agentProfileId: "agent-2",
executorProfileId: "exec-profile-1",
synced: true,
});
});
it("preserves queued task-create fields when multiple callers join the same fetch", async () => {
let resolveFetch: (value: unknown) => void = () => undefined;
mockReadQueuedTaskCreateLastUsedState.mockReturnValue({
repositoryId: undefined,
branch: "feature",
agentProfileId: undefined,
executorProfileId: undefined,
});
mockFetchUserSettings.mockReturnValue(
new Promise((resolve) => {
resolveFetch = resolve;
}),
);
renderHook(() => useEnsureUserSettings(true));
renderHook(() => useEnsureUserSettings(true));
resolveFetch(
userSettingsResponse({
repository_id: "repo-1",
branch: "main",
agent_profile_id: "agent-1",
}),
);
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalledTimes(2));
expect(mockSetUserSettings.mock.calls.at(-1)![0].taskCreateLastUsed).toMatchObject({
repositoryId: "repo-1",
branch: "feature",
agentProfileId: "agent-1",
});
});
it("does not fetch while disabled", () => {
const { result } = renderHook(() => useEnsureUserSettings(false));
expect(mockFetchUserSettings).not.toHaveBeenCalled();
expect(result.current.loaded).toBe(false);
});
it("settles a failed fetch for the current mount but retries on the next mount", async () => {
mockFetchUserSettings
.mockRejectedValueOnce(new Error("temporary"))
.mockResolvedValueOnce(userSettingsResponse({ repository_id: "repo-2" }));
const first = renderHook(() => useEnsureUserSettings(true));
await waitFor(() => expect(first.result.current.loaded).toBe(true));
expect(mockSetUserSettings).not.toHaveBeenCalled();
first.unmount();
renderHook(() => useEnsureUserSettings(true));
await waitFor(() => expect(mockFetchUserSettings).toHaveBeenCalledTimes(2));
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
expect(mockSetUserSettings.mock.calls[0]![0].taskCreateLastUsed.repositoryId).toBe("repo-2");
});
});
describe("useEnsureUserSettings — loaded settings overlay", () => {
it("applies queued task-create fields when settings are already loaded", () => {
mockState.userSettings = {
...makeUnloadedSettings(),
loaded: true,
taskCreateLastUsed: {
repositoryId: "repo-1",
branch: "main",
agentProfileId: "agent-1",
executorProfileId: "exec-1",
},
};
mockReadQueuedTaskCreateLastUsedState.mockReturnValue({
repositoryId: "repo-2",
branch: "feature",
agentProfileId: undefined,
executorProfileId: undefined,
});
const { result } = renderHook(() => useEnsureUserSettings(true));
expect(mockFetchUserSettings).not.toHaveBeenCalled();
expect(result.current.userSettings.taskCreateLastUsed).toEqual({
repositoryId: "repo-2",
branch: "feature",
agentProfileId: "agent-1",
executorProfileId: "exec-1",
});
});
});
describe("useEnsureUserSettings — stale settings fetches", () => {
it("keeps task-create edits made while a settings fetch is in flight", async () => {
let resolveFetch: (value: unknown) => void = () => undefined;
mockFetchUserSettings.mockReturnValue(
new Promise((resolve) => {
resolveFetch = resolve;
}),
);
renderHook(() => useEnsureUserSettings(true));
await waitFor(() => expect(mockFetchUserSettings).toHaveBeenCalledTimes(1));
mockReadQueuedTaskCreateLastUsedState.mockReturnValue({
repositoryId: "repo-2",
branch: "feature",
agentProfileId: undefined,
executorProfileId: undefined,
});
resolveFetch(
userSettingsResponse({
repository_id: "repo-1",
branch: "main",
}),
);
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
expect(mockSetUserSettings.mock.calls[0]![0].taskCreateLastUsed).toMatchObject({
repositoryId: "repo-2",
branch: "feature",
});
});
});