Files
kandev/apps/web/components/github/my-github/use-sidebar-selection.test.ts
2026-08-11 11:52:34 +01:00

410 lines
13 KiB
TypeScript

import { act, renderHook, waitFor } from "@testing-library/react";
import { IconInbox } from "@tabler/icons-react";
import { describe, expect, it, vi } from "vitest";
import type { PresetOption } from "./search-bar";
import type { SidebarSelection } from "./presets-sidebar";
import type { SavedPreset } from "./saved-preset-model";
import { useInitialSidebarSelection, useSidebarSelectionHandler } from "./use-sidebar-selection";
const prPreset: PresetOption = {
value: "review-requested",
label: "Review requested",
filter: "review-requested:@me is:open",
group: "inbox",
icon: IconInbox,
};
const issuePreset: PresetOption = {
value: "assigned",
label: "Assigned",
filter: "assignee:@me is:open",
group: "inbox",
icon: IconInbox,
};
const prPresets = [prPreset];
const issuePresets = [issuePreset];
const WORKSPACE_ID = "workspace-1";
const KIND_SWITCH = "kind-switch" as const;
type HandlerRenderProps = {
savedPresets: SavedPreset[];
currentPrPresets: PresetOption[];
currentIssuePresets: PresetOption[];
currentKind: SidebarSelection["kind"];
};
const prDefault: SavedPreset = {
id: "saved-pr",
kind: "pr",
label: "Kandev PRs",
customQuery: "author:@me is:open",
repoFilter: "kdlbs/kandev",
createdAt: "2026-08-07T00:00:00Z",
isDefault: true,
};
const issueDefault: SavedPreset = {
...prDefault,
id: "saved-issue",
kind: "issue",
label: "Kandev issues",
customQuery: "assignee:@me label:bug",
};
function createAutoResetControls() {
let enabled = true;
return {
shouldAutoResetSearch: () => enabled,
resetSearchOnWorkspaceChange: vi.fn(() => {
enabled = true;
}),
disable: () => {
enabled = false;
},
};
}
async function expectNewWorkspaceDefaultAfterManualSelection() {
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const autoReset = createAutoResetControls();
const { result, rerender } = renderHook(
({ workspaceId, savedPresets }) =>
useInitialSidebarSelection({
workspaceId,
resolvedPrPresets: prPresets,
shouldAutoResetSearch: autoReset.shouldAutoResetSearch,
resetSearchOnWorkspaceChange: autoReset.resetSearchOnWorkspaceChange,
setQueryImmediate,
setRepoFilter,
savedPresets,
}),
{
initialProps: {
workspaceId: WORKSPACE_ID,
savedPresets: [] as SavedPreset[],
},
},
);
const manual: SidebarSelection = { kind: "pr", source: "preset", id: "manual" };
act(() => {
result.current.setUserSelection(manual);
autoReset.disable();
});
autoReset.resetSearchOnWorkspaceChange.mockClear();
rerender({ workspaceId: "workspace-2", savedPresets: [prDefault] });
await waitFor(() => expect(result.current.selection.id).toBe(prDefault.id));
expect(setQueryImmediate).toHaveBeenLastCalledWith(prDefault.customQuery);
expect(setRepoFilter).toHaveBeenLastCalledWith(prDefault.repoFilter);
expect(autoReset.resetSearchOnWorkspaceChange).toHaveBeenCalledOnce();
}
function expectConfiguredPresetFallbackOnKindSwitch() {
const setUserSelection = vi.fn();
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const { result } = renderHook(() =>
useSidebarSelectionHandler({
currentKind: "pr",
savedPresets: [],
resolvedPrPresets: prPresets,
resolvedIssuePresets: issuePresets,
setQueryImmediate,
setRepoFilter,
setUserSelection,
markSearchInteracted: vi.fn(),
}),
);
act(() => result.current({ kind: "issue", source: KIND_SWITCH }));
expect(setUserSelection).toHaveBeenCalledWith({
kind: "issue",
source: "preset",
id: issuePreset.value,
});
expect(setQueryImmediate).toHaveBeenCalledWith(issuePreset.filter);
expect(setRepoFilter).toHaveBeenCalledWith("");
}
function expectSameKindSentinelNoop() {
const setUserSelection = vi.fn();
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const markSearchInteracted = vi.fn();
const { result } = renderHook(() =>
useSidebarSelectionHandler({
currentKind: "pr",
savedPresets: [],
resolvedPrPresets: prPresets,
resolvedIssuePresets: issuePresets,
setQueryImmediate,
setRepoFilter,
setUserSelection,
markSearchInteracted,
}),
);
act(() => result.current({ kind: "pr", source: KIND_SWITCH }));
expect(markSearchInteracted).not.toHaveBeenCalled();
expect(setUserSelection).not.toHaveBeenCalled();
expect(setQueryImmediate).not.toHaveBeenCalled();
expect(setRepoFilter).not.toHaveBeenCalled();
}
function expectStableHandlerWithRefreshedPresetCatalogs() {
const setUserSelection = vi.fn();
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const markSearchInteracted = vi.fn();
const { result, rerender } = renderHook(
({ savedPresets, currentPrPresets, currentIssuePresets, currentKind }: HandlerRenderProps) =>
useSidebarSelectionHandler({
currentKind,
savedPresets,
resolvedPrPresets: currentPrPresets,
resolvedIssuePresets: currentIssuePresets,
setQueryImmediate,
setRepoFilter,
setUserSelection,
markSearchInteracted,
}),
{
initialProps: {
savedPresets: [prDefault],
currentPrPresets: prPresets,
currentIssuePresets: issuePresets,
currentKind: "pr",
},
},
);
const initialHandler = result.current;
rerender({
savedPresets: [prDefault, issueDefault],
currentPrPresets: [...prPresets],
currentIssuePresets: [...issuePresets],
currentKind: "pr",
});
expect(result.current).toBe(initialHandler);
act(() => initialHandler({ kind: "issue", source: KIND_SWITCH }));
expect(setUserSelection).toHaveBeenCalledWith({
kind: "issue",
source: "saved",
id: issueDefault.id,
});
expect(setQueryImmediate).toHaveBeenCalledWith(issueDefault.customQuery);
expect(setRepoFilter).toHaveBeenCalledWith(issueDefault.repoFilter);
const refreshedIssuePreset = {
...issuePreset,
value: "mentioned",
filter: "mentions:@me is:open",
};
setUserSelection.mockClear();
setQueryImmediate.mockClear();
setRepoFilter.mockClear();
rerender({
savedPresets: [prDefault],
currentPrPresets: [...prPresets],
currentIssuePresets: [refreshedIssuePreset],
currentKind: "pr",
});
expect(result.current).toBe(initialHandler);
act(() => initialHandler({ kind: "issue", source: KIND_SWITCH }));
expect(setUserSelection).toHaveBeenCalledWith({
kind: "issue",
source: "preset",
id: refreshedIssuePreset.value,
});
expect(setQueryImmediate).toHaveBeenCalledWith(refreshedIssuePreset.filter);
expect(setRepoFilter).toHaveBeenCalledWith("");
markSearchInteracted.mockClear();
setUserSelection.mockClear();
setQueryImmediate.mockClear();
setRepoFilter.mockClear();
rerender({
savedPresets: [prDefault],
currentPrPresets: [...prPresets],
currentIssuePresets: [refreshedIssuePreset],
currentKind: "issue",
});
expect(result.current).toBe(initialHandler);
act(() => initialHandler({ kind: "issue", source: KIND_SWITCH }));
expect(markSearchInteracted).not.toHaveBeenCalled();
expect(setUserSelection).not.toHaveBeenCalled();
expect(setQueryImmediate).not.toHaveBeenCalled();
expect(setRepoFilter).not.toHaveBeenCalled();
}
describe("useInitialSidebarSelection", () => {
it("does not reapply an unchanged default when preset references change", async () => {
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const autoReset = createAutoResetControls();
const { rerender } = renderHook(
({ savedPresets }) =>
useInitialSidebarSelection({
workspaceId: WORKSPACE_ID,
resolvedPrPresets: prPresets,
shouldAutoResetSearch: autoReset.shouldAutoResetSearch,
resetSearchOnWorkspaceChange: autoReset.resetSearchOnWorkspaceChange,
setQueryImmediate,
setRepoFilter,
savedPresets,
}),
{ initialProps: { savedPresets: [prDefault] } },
);
await waitFor(() => expect(setQueryImmediate).toHaveBeenCalledOnce());
rerender({ savedPresets: [{ ...prDefault }] });
expect(setQueryImmediate).toHaveBeenCalledOnce();
expect(setRepoFilter).toHaveBeenCalledOnce();
});
it("applies a saved PR default when saved queries hydrate", async () => {
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const autoReset = createAutoResetControls();
const { result, rerender } = renderHook(
({ savedPresets }) =>
useInitialSidebarSelection({
workspaceId: WORKSPACE_ID,
resolvedPrPresets: prPresets,
shouldAutoResetSearch: autoReset.shouldAutoResetSearch,
resetSearchOnWorkspaceChange: autoReset.resetSearchOnWorkspaceChange,
setQueryImmediate,
setRepoFilter,
savedPresets,
}),
{ initialProps: { savedPresets: [] as SavedPreset[] } },
);
await waitFor(() => expect(result.current.selection.id).toBe(prPreset.value));
rerender({ savedPresets: [prDefault] });
await waitFor(() =>
expect(result.current.selection).toEqual({
kind: "pr",
source: "saved",
id: prDefault.id,
}),
);
expect(setQueryImmediate).toHaveBeenLastCalledWith(prDefault.customQuery);
expect(setRepoFilter).toHaveBeenLastCalledWith(prDefault.repoFilter);
});
it("does not apply a late default after manual selection", async () => {
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const autoReset = createAutoResetControls();
const { result, rerender } = renderHook(
({ savedPresets }) =>
useInitialSidebarSelection({
workspaceId: WORKSPACE_ID,
resolvedPrPresets: prPresets,
shouldAutoResetSearch: autoReset.shouldAutoResetSearch,
resetSearchOnWorkspaceChange: autoReset.resetSearchOnWorkspaceChange,
setQueryImmediate,
setRepoFilter,
savedPresets,
}),
{ initialProps: { savedPresets: [] as SavedPreset[] } },
);
const manual: SidebarSelection = { kind: "pr", source: "preset", id: "manual" };
act(() => result.current.setUserSelection(manual));
setQueryImmediate.mockClear();
setRepoFilter.mockClear();
rerender({ savedPresets: [prDefault] });
expect(result.current.selection).toEqual(manual);
expect(setQueryImmediate).not.toHaveBeenCalled();
expect(setRepoFilter).not.toHaveBeenCalled();
});
it("resets manual selection before applying a new workspace default", async () => {
await expectNewWorkspaceDefaultAfterManualSelection();
});
});
describe("useSidebarSelectionHandler", () => {
it("applies the destination kind's saved default on a kind switch", () => {
const setUserSelection = vi.fn();
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const markSearchInteracted = vi.fn();
const { result } = renderHook(() =>
useSidebarSelectionHandler({
currentKind: "pr",
savedPresets: [prDefault, issueDefault],
resolvedPrPresets: prPresets,
resolvedIssuePresets: issuePresets,
setQueryImmediate,
setRepoFilter,
setUserSelection,
markSearchInteracted,
}),
);
act(() => result.current({ kind: "issue", source: KIND_SWITCH }));
expect(markSearchInteracted).toHaveBeenCalledOnce();
expect(setUserSelection).toHaveBeenCalledWith({
kind: "issue",
source: "saved",
id: issueDefault.id,
});
expect(setQueryImmediate).toHaveBeenCalledWith(issueDefault.customQuery);
expect(setRepoFilter).toHaveBeenCalledWith(issueDefault.repoFilter);
});
it(
"falls back to the first configured preset when the destination has no saved default",
expectConfiguredPresetFallbackOnKindSwitch,
);
it("ignores a same-kind toggle sentinel", expectSameKindSentinelNoop);
it(
"keeps the handler stable while using refreshed catalogs and current kind",
expectStableHandlerWithRefreshedPresetCatalogs,
);
it("keeps an explicit selection within the current kind", () => {
const setUserSelection = vi.fn();
const setQueryImmediate = vi.fn();
const setRepoFilter = vi.fn();
const requested: SidebarSelection = {
kind: "pr",
source: "preset",
id: prPreset.value,
};
const { result } = renderHook(() =>
useSidebarSelectionHandler({
currentKind: "pr",
savedPresets: [prDefault],
resolvedPrPresets: prPresets,
resolvedIssuePresets: issuePresets,
setQueryImmediate,
setRepoFilter,
setUserSelection,
markSearchInteracted: vi.fn(),
}),
);
act(() => result.current(requested));
expect(setUserSelection).toHaveBeenCalledWith(requested);
expect(setQueryImmediate).toHaveBeenCalledWith(prPreset.filter);
expect(setRepoFilter).toHaveBeenCalledWith("");
});
});