410 lines
13 KiB
TypeScript
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("");
|
|
});
|
|
});
|