mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 12:35:35 +02:00
MUL-4818: keep Inbox and sidebar unread counts static (#5461)
* fix(sidebar): keep unread counters static Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): keep title unread count static Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
72
packages/views/inbox/components/inbox-page.test.tsx
Normal file
72
packages/views/inbox/components/inbox-page.test.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { InboxPage } from "./inbox-page";
|
||||
|
||||
vi.mock("react-resizable-panels", () => ({
|
||||
useDefaultLayout: () => ({ defaultLayout: undefined, onLayoutChanged: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: [], isLoading: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/hooks", () => ({
|
||||
useWorkspaceId: () => "workspace-1",
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/paths", () => ({
|
||||
useWorkspacePaths: () => ({
|
||||
inbox: () => "/acme/inbox",
|
||||
issueDetail: (id: string) => `/acme/issues/${id}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/modals", () => ({
|
||||
useModalStore: { getState: () => ({ open: vi.fn() }) },
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/issues/stores/draft-store", () => ({
|
||||
useIssueDraftStore: { getState: () => ({ setDraft: vi.fn() }) },
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/inbox/queries", () => ({
|
||||
inboxListOptions: () => ({ queryKey: ["inbox"] }),
|
||||
deduplicateInboxItems: (items: unknown[]) => items,
|
||||
useInboxUnreadCount: () => 2,
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/inbox/mutations", () => {
|
||||
const mutation = () => ({ mutate: vi.fn() });
|
||||
return {
|
||||
useMarkInboxRead: mutation,
|
||||
useArchiveInbox: mutation,
|
||||
useMarkAllInboxRead: mutation,
|
||||
useArchiveAllInbox: mutation,
|
||||
useArchiveAllReadInbox: mutation,
|
||||
useArchiveCompletedInbox: mutation,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../issues/components", () => ({ IssueDetail: () => null }));
|
||||
|
||||
vi.mock("../../navigation", () => ({
|
||||
useNavigation: () => ({ searchParams: new URLSearchParams(), replace: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/ui/hooks/use-mobile", () => ({ useIsMobile: () => true }));
|
||||
vi.mock("./inbox-list", () => ({ InboxList: () => null }));
|
||||
vi.mock("./inbox-list-item", () => ({ useTimeAgo: () => vi.fn() }));
|
||||
vi.mock("./inbox-detail-label", () => ({ useTypeLabels: () => ({}) }));
|
||||
vi.mock("../../i18n", () => ({ useT: () => ({ t: () => "Inbox" }) }));
|
||||
|
||||
describe("InboxPage", () => {
|
||||
it("keeps the title unread count static", () => {
|
||||
const { container } = render(<InboxPage />);
|
||||
const titleCount = container.querySelector("h1")?.parentElement?.querySelector(
|
||||
"number-flow-react",
|
||||
) as (HTMLElement & { animated?: boolean }) | null;
|
||||
|
||||
expect(titleCount?.getAttribute("aria-label")).toBe("2");
|
||||
expect(titleCount?.animated).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -231,6 +231,7 @@ export function InboxPage() {
|
||||
{unreadCount > 0 && (
|
||||
<NumberFlow
|
||||
value={unreadCount}
|
||||
animated={false}
|
||||
format={{ maximumFractionDigits: 0 }}
|
||||
aria-label={String(unreadCount)}
|
||||
className="text-xs text-muted-foreground"
|
||||
|
||||
@@ -3,12 +3,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ApiError } from "@multica/core/api";
|
||||
import { AppSidebar } from "./app-sidebar";
|
||||
|
||||
const { appForeground, chatSessions, chatStore, detail, deletePin, navigation, pins, summary, workspaces } = vi.hoisted(() => ({
|
||||
const { appForeground, chatSessions, chatStore, detail, deletePin, inboxItems, navigation, pins, summary, workspaces } = vi.hoisted(() => ({
|
||||
appForeground: { current: true },
|
||||
chatSessions: { current: [] as { id?: string; unread_count?: number }[] },
|
||||
chatStore: { current: { activeSessionId: null as string | null, isOpen: false } },
|
||||
detail: { current: { isPending: false, isError: false, data: null as unknown, error: null as unknown } },
|
||||
deletePin: vi.fn(),
|
||||
inboxItems: { current: [] as { id: string; read: boolean }[] },
|
||||
navigation: { current: { pathname: "/acme/issues" } },
|
||||
summary: { current: [] as { workspace_id: string; count: number }[] },
|
||||
workspaces: {
|
||||
@@ -174,6 +175,7 @@ vi.mock("@tanstack/react-query", async (importOriginal) => ({
|
||||
if (queryKey[0] === "pins") return { data: pins.current };
|
||||
if (queryKey[0] === "issue") return detail.current;
|
||||
if (queryKey[0] === "inbox" && queryKey[1] === "unread-summary") return { data: summary.current };
|
||||
if (queryKey[0] === "inbox") return { data: inboxItems.current };
|
||||
if (queryKey[0] === "workspaces") return { data: workspaces.current };
|
||||
if (queryKey[0] === "chat" && queryKey[2] === "sessions") return { data: chatSessions.current };
|
||||
return { data: [] };
|
||||
@@ -298,6 +300,7 @@ describe("workspace-switcher dropdown per-workspace dot", () => {
|
||||
describe("personal nav — Chat", () => {
|
||||
beforeEach(() => {
|
||||
chatSessions.current = [];
|
||||
inboxItems.current = [];
|
||||
navigation.current = { pathname: "/acme/issues" };
|
||||
chatStore.current = { activeSessionId: null, isOpen: false };
|
||||
appForeground.current = true;
|
||||
@@ -310,6 +313,19 @@ describe("personal nav — Chat", () => {
|
||||
const chatBadge = (container: HTMLElement) =>
|
||||
chatNav(container)?.querySelector("number-flow-react") ?? null;
|
||||
|
||||
it("keeps persistent Inbox and Chat counters static", () => {
|
||||
inboxItems.current = [{ id: "inbox-1", read: false }];
|
||||
chatSessions.current = [{ id: "chat-1", unread_count: 2 }];
|
||||
const { container } = render(<AppSidebar />);
|
||||
const inboxBadge = container
|
||||
.querySelector<HTMLElement>('button[data-href="/acme/inbox"]')
|
||||
?.querySelector("number-flow-react") as (HTMLElement & { animated?: boolean }) | null;
|
||||
const currentChatBadge = chatBadge(container) as (HTMLElement & { animated?: boolean }) | null;
|
||||
|
||||
expect(inboxBadge?.animated).toBe(false);
|
||||
expect(currentChatBadge?.animated).toBe(false);
|
||||
});
|
||||
|
||||
it("renders a Chat nav link to the workspace chat route", () => {
|
||||
const { container } = render(<AppSidebar />);
|
||||
expect(chatNav(container)).not.toBeNull();
|
||||
|
||||
@@ -683,12 +683,14 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
|
||||
{item.key === "inbox" && unreadCount > 0 && (
|
||||
<CappedNumberFlow
|
||||
value={unreadCount}
|
||||
animated={false}
|
||||
className="ml-auto text-xs"
|
||||
/>
|
||||
)}
|
||||
{item.key === "chat" && chatUnreadCount > 0 && (
|
||||
<CappedNumberFlow
|
||||
value={chatUnreadCount}
|
||||
animated={false}
|
||||
className="ml-auto text-xs"
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user