Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit af433df

Browse files
committed
add sidebar nav and customize dialog tests
1 parent 9529748 commit af433df

3 files changed

Lines changed: 276 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
2+
import { Theme } from "@radix-ui/themes";
3+
import { render, screen } from "@testing-library/react";
4+
import userEvent from "@testing-library/user-event";
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
6+
7+
const { track } = vi.hoisted(() => ({ track: vi.fn() }));
8+
9+
vi.mock("@posthog/ui/shell/analytics", () => ({ track }));
10+
11+
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
12+
import { CustomizeSidebarDialog } from "./CustomizeSidebarDialog";
13+
14+
function renderDialog() {
15+
return render(
16+
<Theme>
17+
<CustomizeSidebarDialog open onOpenChange={vi.fn()} />
18+
</Theme>,
19+
);
20+
}
21+
22+
describe("CustomizeSidebarDialog", () => {
23+
beforeEach(() => {
24+
track.mockReset();
25+
useSidebarStore.setState({ navItemOverrides: {} });
26+
});
27+
28+
it("unchecking a visible item demotes it and tracks the change", async () => {
29+
const user = userEvent.setup();
30+
renderDialog();
31+
32+
await user.click(screen.getByRole("checkbox", { name: "MCP servers" }));
33+
34+
expect(useSidebarStore.getState().navItemOverrides["mcp-servers"]).toBe(
35+
false,
36+
);
37+
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.SIDEBAR_CUSTOMIZED, {
38+
item: "mcp_servers",
39+
visible: false,
40+
});
41+
});
42+
43+
it("checking a hidden item promotes it and tracks the change", async () => {
44+
const user = userEvent.setup();
45+
renderDialog();
46+
47+
await user.click(screen.getByRole("checkbox", { name: "Search" }));
48+
49+
expect(useSidebarStore.getState().navItemOverrides.search).toBe(true);
50+
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.SIDEBAR_CUSTOMIZED, {
51+
item: "search",
52+
visible: true,
53+
});
54+
});
55+
});
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
2+
import { Theme } from "@radix-ui/themes";
3+
import { render, screen } from "@testing-library/react";
4+
import userEvent from "@testing-library/user-event";
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
6+
7+
if (typeof globalThis.ResizeObserver === "undefined") {
8+
globalThis.ResizeObserver = class {
9+
observe() {}
10+
unobserve() {}
11+
disconnect() {}
12+
} as unknown as typeof ResizeObserver;
13+
}
14+
15+
const {
16+
track,
17+
useAppView,
18+
navigateToInbox,
19+
navigateToAgents,
20+
navigateToSkills,
21+
navigateToMcpServers,
22+
navigateToCommandCenter,
23+
navigateToActivity,
24+
openCommandMenu,
25+
} = vi.hoisted(() => ({
26+
track: vi.fn(),
27+
useAppView: vi.fn(),
28+
navigateToInbox: vi.fn(),
29+
navigateToAgents: vi.fn(),
30+
navigateToSkills: vi.fn(),
31+
navigateToMcpServers: vi.fn(),
32+
navigateToCommandCenter: vi.fn(),
33+
navigateToActivity: vi.fn(),
34+
openCommandMenu: vi.fn(),
35+
}));
36+
37+
vi.mock("@posthog/ui/shell/analytics", () => ({ track }));
38+
vi.mock("@posthog/ui/router/useAppView", () => ({ useAppView }));
39+
vi.mock("@posthog/ui/features/feature-flags/useFeatureFlag", () => ({
40+
useFeatureFlag: () => true,
41+
}));
42+
vi.mock("@posthog/ui/router/navigationBridge", () => ({
43+
navigateToActivity,
44+
navigateToAgents,
45+
navigateToCommandCenter,
46+
navigateToHome: vi.fn(),
47+
navigateToInbox,
48+
navigateToLoops: vi.fn(),
49+
navigateToMcpServers,
50+
navigateToSkills,
51+
navigateToWebsiteCommandCenter: vi.fn(),
52+
navigateToWebsiteHome: vi.fn(),
53+
navigateToWebsiteMcpServers: vi.fn(),
54+
navigateToWebsiteSkills: vi.fn(),
55+
}));
56+
vi.mock("@posthog/ui/router/useOpenTask", () => ({ openTaskInput: vi.fn() }));
57+
vi.mock("@posthog/ui/shell/commandMenuStore", () => ({
58+
useCommandMenuStore: (selector: (s: { open: () => void }) => unknown) =>
59+
selector({ open: openCommandMenu }),
60+
}));
61+
vi.mock("@posthog/ui/features/command-center/commandCenterStore", () => ({
62+
useCommandCenterStore: (
63+
selector: (s: { cells: (string | null)[] }) => unknown,
64+
) => selector({ cells: [] }),
65+
}));
66+
vi.mock("@posthog/ui/features/inbox/hooks/useInboxAllReports", () => ({
67+
useInboxAllReports: () => ({ counts: { pulls: 0 } }),
68+
}));
69+
vi.mock("@posthog/ui/features/tasks/useTasks", () => ({
70+
useTasks: () => ({ data: [] }),
71+
}));
72+
vi.mock("@posthog/ui/features/canvas/hooks/useMentionActivity", () => ({
73+
useMentionActivity: () => ({ items: [] }),
74+
}));
75+
vi.mock("@posthog/ui/features/canvas/stores/activitySeenStore", () => ({
76+
useActivitySeenStore: (
77+
selector: (s: { lastSeenAt: number | null }) => unknown,
78+
) => selector({ lastSeenAt: null }),
79+
}));
80+
vi.mock("@tanstack/react-router", () => ({
81+
useRouterState: () => false,
82+
}));
83+
84+
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
85+
import { SidebarNavSection } from "./SidebarNavSection";
86+
87+
function renderNav() {
88+
return render(
89+
<Theme>
90+
<SidebarNavSection />
91+
</Theme>,
92+
);
93+
}
94+
95+
describe("SidebarNavSection", () => {
96+
beforeEach(() => {
97+
vi.clearAllMocks();
98+
useAppView.mockReturnValue({ type: "home" });
99+
useSidebarStore.setState({ navItemOverrides: {}, channelsEnabled: true });
100+
});
101+
102+
it.each([
103+
["search", "Search"],
104+
["inbox", "Inbox"],
105+
["agents", "Agents"],
106+
["skills", "Skills"],
107+
["mcp-servers", "MCP servers"],
108+
["command-center", "Command Center"],
109+
["contexts", "Channels"],
110+
["activity", "Activity"],
111+
] as const)(
112+
"moves %s from the top level into More when hidden",
113+
async (id, label) => {
114+
const user = userEvent.setup();
115+
useSidebarStore.setState({ navItemOverrides: { [id]: false } });
116+
renderNav();
117+
118+
expect(screen.queryByText(label)).not.toBeInTheDocument();
119+
120+
await user.click(screen.getByRole("button", { name: "More" }));
121+
122+
expect(screen.getByText(label)).toBeInTheDocument();
123+
},
124+
);
125+
126+
it.each([
127+
["inbox", "inbox", "Inbox"],
128+
["agents", "agents", "Agents"],
129+
["skills", "skills", "Skills"],
130+
["mcp-servers", "mcp-servers", "MCP servers"],
131+
["command-center", "command-center", "Command Center"],
132+
["activity", "activity", "Activity"],
133+
] as const)(
134+
"active hidden %s takes over the collapsed More row",
135+
(id, viewType, label) => {
136+
useAppView.mockReturnValue({ type: viewType });
137+
useSidebarStore.setState({ navItemOverrides: { [id]: false } });
138+
renderNav();
139+
140+
expect(
141+
screen.queryByRole("button", { name: "More" }),
142+
).not.toBeInTheDocument();
143+
expect(screen.getByRole("button", { name: label })).toBeInTheDocument();
144+
},
145+
);
146+
147+
it("never lets hidden search take over the More row", () => {
148+
useSidebarStore.setState({ navItemOverrides: { search: false } });
149+
renderNav();
150+
151+
expect(screen.getByRole("button", { name: "More" })).toBeInTheDocument();
152+
});
153+
154+
it("tracks top-level clicks with in_more false", async () => {
155+
const user = userEvent.setup();
156+
renderNav();
157+
158+
await user.click(screen.getByRole("button", { name: /Inbox/ }));
159+
160+
expect(navigateToInbox).toHaveBeenCalledTimes(1);
161+
expect(track).toHaveBeenCalledWith(
162+
ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED,
163+
{ item: "inbox", in_more: false },
164+
);
165+
});
166+
167+
it("tracks clicks inside the expanded More section with in_more true", async () => {
168+
const user = userEvent.setup();
169+
useSidebarStore.setState({ navItemOverrides: { inbox: false } });
170+
renderNav();
171+
172+
await user.click(screen.getByRole("button", { name: "More" }));
173+
await user.click(screen.getByRole("button", { name: /Inbox/ }));
174+
175+
expect(navigateToInbox).toHaveBeenCalledTimes(1);
176+
expect(track).toHaveBeenCalledWith(
177+
ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED,
178+
{ item: "inbox", in_more: true },
179+
);
180+
});
181+
182+
it.each([
183+
[false, true, "enter_space"],
184+
[true, false, "leave_space"],
185+
] as const)(
186+
"toggling contexts from %s tracks the toggle and %s",
187+
async (initial, expected, spaceAction) => {
188+
const user = userEvent.setup();
189+
useSidebarStore.setState({ channelsEnabled: initial });
190+
renderNav();
191+
192+
await user.click(screen.getByRole("switch"));
193+
194+
expect(useSidebarStore.getState().channelsEnabled).toBe(expected);
195+
expect(track).toHaveBeenCalledWith(
196+
ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED,
197+
{ item: "contexts", in_more: false },
198+
);
199+
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.CHANNEL_ACTION, {
200+
action_type: "toggle_channels",
201+
surface: "nav",
202+
});
203+
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.CHANNEL_ACTION, {
204+
action_type: spaceAction,
205+
surface: "nav",
206+
});
207+
},
208+
);
209+
});

packages/ui/src/features/sidebar/sidebarStore.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ describe("sidebarStore navItemOverrides", () => {
6161
},
6262
);
6363

64+
it("rehydration falls back to defaults when persisted state predates overrides", async () => {
65+
localStorage.setItem(
66+
"sidebar-storage",
67+
JSON.stringify({ state: { open: true }, version: 0 }),
68+
);
69+
70+
await useSidebarStore.persist.rehydrate();
71+
72+
expect(useSidebarStore.getState().navItemOverrides).toEqual({});
73+
localStorage.removeItem("sidebar-storage");
74+
});
75+
6476
it("rehydration drops unknown ids and non-boolean values", async () => {
6577
localStorage.setItem(
6678
"sidebar-storage",

0 commit comments

Comments
 (0)