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

Commit 89abe47

Browse files
committed
add sidebar nav and customize dialog tests
1 parent e1b133b commit 89abe47

3 files changed

Lines changed: 275 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: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
navigateToMcpServers,
49+
navigateToSkills,
50+
navigateToWebsiteCommandCenter: vi.fn(),
51+
navigateToWebsiteHome: vi.fn(),
52+
navigateToWebsiteMcpServers: vi.fn(),
53+
navigateToWebsiteSkills: vi.fn(),
54+
}));
55+
vi.mock("@posthog/ui/router/useOpenTask", () => ({ openTaskInput: vi.fn() }));
56+
vi.mock("@posthog/ui/shell/commandMenuStore", () => ({
57+
useCommandMenuStore: (selector: (s: { open: () => void }) => unknown) =>
58+
selector({ open: openCommandMenu }),
59+
}));
60+
vi.mock("@posthog/ui/features/command-center/commandCenterStore", () => ({
61+
useCommandCenterStore: (
62+
selector: (s: { cells: (string | null)[] }) => unknown,
63+
) => selector({ cells: [] }),
64+
}));
65+
vi.mock("@posthog/ui/features/inbox/hooks/useInboxAllReports", () => ({
66+
useInboxAllReports: () => ({ counts: { pulls: 0 } }),
67+
}));
68+
vi.mock("@posthog/ui/features/tasks/useTasks", () => ({
69+
useTasks: () => ({ data: [] }),
70+
}));
71+
vi.mock("@posthog/ui/features/canvas/hooks/useMentionActivity", () => ({
72+
useMentionActivity: () => ({ items: [] }),
73+
}));
74+
vi.mock("@posthog/ui/features/canvas/stores/activitySeenStore", () => ({
75+
useActivitySeenStore: (
76+
selector: (s: { lastSeenAt: number | null }) => unknown,
77+
) => selector({ lastSeenAt: null }),
78+
}));
79+
vi.mock("@tanstack/react-router", () => ({
80+
useRouterState: () => false,
81+
}));
82+
83+
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
84+
import { SidebarNavSection } from "./SidebarNavSection";
85+
86+
function renderNav() {
87+
return render(
88+
<Theme>
89+
<SidebarNavSection />
90+
</Theme>,
91+
);
92+
}
93+
94+
describe("SidebarNavSection", () => {
95+
beforeEach(() => {
96+
vi.clearAllMocks();
97+
useAppView.mockReturnValue({ type: "home" });
98+
useSidebarStore.setState({ navItemOverrides: {}, channelsEnabled: true });
99+
});
100+
101+
it.each([
102+
["search", "Search"],
103+
["inbox", "Inbox"],
104+
["agents", "Agents"],
105+
["skills", "Skills"],
106+
["mcp-servers", "MCP servers"],
107+
["command-center", "Command Center"],
108+
["contexts", "Contexts"],
109+
["activity", "Activity"],
110+
] as const)(
111+
"moves %s from the top level into More when hidden",
112+
async (id, label) => {
113+
const user = userEvent.setup();
114+
useSidebarStore.setState({ navItemOverrides: { [id]: false } });
115+
renderNav();
116+
117+
expect(screen.queryByText(label)).not.toBeInTheDocument();
118+
119+
await user.click(screen.getByRole("button", { name: "More" }));
120+
121+
expect(screen.getByText(label)).toBeInTheDocument();
122+
},
123+
);
124+
125+
it.each([
126+
["inbox", "inbox", "Inbox"],
127+
["agents", "agents", "Agents"],
128+
["skills", "skills", "Skills"],
129+
["mcp-servers", "mcp-servers", "MCP servers"],
130+
["command-center", "command-center", "Command Center"],
131+
["activity", "activity", "Activity"],
132+
] as const)(
133+
"active hidden %s takes over the collapsed More row",
134+
(id, viewType, label) => {
135+
useAppView.mockReturnValue({ type: viewType });
136+
useSidebarStore.setState({ navItemOverrides: { [id]: false } });
137+
renderNav();
138+
139+
expect(
140+
screen.queryByRole("button", { name: "More" }),
141+
).not.toBeInTheDocument();
142+
expect(screen.getByRole("button", { name: label })).toBeInTheDocument();
143+
},
144+
);
145+
146+
it("never lets hidden search take over the More row", () => {
147+
useSidebarStore.setState({ navItemOverrides: { search: false } });
148+
renderNav();
149+
150+
expect(screen.getByRole("button", { name: "More" })).toBeInTheDocument();
151+
});
152+
153+
it("tracks top-level clicks with in_more false", async () => {
154+
const user = userEvent.setup();
155+
renderNav();
156+
157+
await user.click(screen.getByRole("button", { name: /Inbox/ }));
158+
159+
expect(navigateToInbox).toHaveBeenCalledTimes(1);
160+
expect(track).toHaveBeenCalledWith(
161+
ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED,
162+
{ item: "inbox", in_more: false },
163+
);
164+
});
165+
166+
it("tracks clicks inside the expanded More section with in_more true", async () => {
167+
const user = userEvent.setup();
168+
useSidebarStore.setState({ navItemOverrides: { inbox: false } });
169+
renderNav();
170+
171+
await user.click(screen.getByRole("button", { name: "More" }));
172+
await user.click(screen.getByRole("button", { name: /Inbox/ }));
173+
174+
expect(navigateToInbox).toHaveBeenCalledTimes(1);
175+
expect(track).toHaveBeenCalledWith(
176+
ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED,
177+
{ item: "inbox", in_more: true },
178+
);
179+
});
180+
181+
it.each([
182+
[false, true, "enter_space"],
183+
[true, false, "leave_space"],
184+
] as const)(
185+
"toggling contexts from %s tracks the toggle and %s",
186+
async (initial, expected, spaceAction) => {
187+
const user = userEvent.setup();
188+
useSidebarStore.setState({ channelsEnabled: initial });
189+
renderNav();
190+
191+
await user.click(screen.getByRole("switch"));
192+
193+
expect(useSidebarStore.getState().channelsEnabled).toBe(expected);
194+
expect(track).toHaveBeenCalledWith(
195+
ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED,
196+
{ item: "contexts", in_more: false },
197+
);
198+
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.CHANNEL_ACTION, {
199+
action_type: "toggle_channels",
200+
surface: "nav",
201+
});
202+
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.CHANNEL_ACTION, {
203+
action_type: spaceAction,
204+
surface: "nav",
205+
});
206+
},
207+
);
208+
});

‎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)