import { act } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createRoot, type Root } from "react-dom/client"; import type { SidebarInitialData } from "@/components/sidebar/types"; import { usePreferredSidebarSnapshotData } from "@/components/sidebar/preferred-sidebar-snapshot-context"; import { buildSidebarInitialData } from "@/lib/sidebar-data"; import type { DocumentRecord } from "@/lib/documents"; import { AppLayoutShell } from "./app-layout-shell"; const mockUseSidebarData = vi.fn(); const mockUseSidebarTreeStream = vi.fn(); const mockUsePreferredSidebarSnapshot = vi.fn(); let capturedSidebarProps: Record | null = null; let capturedBreadcrumbProps: Record | null = null; let capturedSearchPaletteProps: Record | null = null; vi.mock("@/hooks/use-sidebar-data", () => ({ useSidebarData: (...args: unknown[]) => mockUseSidebarData(...args), })); vi.mock("@/lib/tree-stream/use-sidebar-tree-stream", () => ({ useSidebarTreeStream: (...args: unknown[]) => mockUseSidebarTreeStream(...args), })); vi.mock("@/components/sidebar/use-preferred-sidebar-snapshot", () => ({ usePreferredSidebarSnapshot: (...args: unknown[]) => mockUsePreferredSidebarSnapshot(...args), })); vi.mock("@/components/sidebar/sidebar", () => ({ Sidebar: (props: Record) => { capturedSidebarProps = props; return
; }, })); vi.mock("@/components/breadcrumb", () => ({ Breadcrumb: (props: Record) => { capturedBreadcrumbProps = props; return
; }, })); vi.mock("@/components/mobile-sidebar-trigger", () => ({ MobileSidebarTrigger: () =>
, })); vi.mock("@/components/search/search-palette", () => ({ SearchPalette: (props: Record) => { capturedSearchPaletteProps = props; return
; }, })); function buildDocument(overrides: Partial = {}): DocumentRecord { return { access_scope: "private", id: "doc-1", workspace_id: "ws-1", title: "标题 A", parent_id: null, sort_order: 0, is_starred: false, is_template: false, created_at: "2026-04-21T00:00:00.000Z", updated_at: "2026-04-21T00:00:00.000Z", ...overrides, }; } function buildSidebarData(documents: DocumentRecord[]): SidebarInitialData { return buildSidebarInitialData({ activeWorkspaceId: "ws-1", workspaces: [], documents, trashedDocuments: [], mindmaps: [], mediaAssets: [], trashedMediaAssets: [], tables: [], }); } function SnapshotProbe() { const snapshot = usePreferredSidebarSnapshotData(); return
{snapshot.documents[0]?.title ?? "空"}
; } describe("AppLayoutShell", () => { let container: HTMLDivElement; let root: Root; beforeEach(() => { (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); capturedSidebarProps = null; capturedBreadcrumbProps = null; capturedSearchPaletteProps = null; mockUseSidebarData.mockReset(); mockUseSidebarTreeStream.mockReset(); mockUsePreferredSidebarSnapshot.mockReset(); }); afterEach(() => { act(() => { root.unmount(); }); container.remove(); }); it("应把同一份 preferred sidebar snapshot 同时提供给 Sidebar 与 Breadcrumb", async () => { const initialData = buildSidebarData([buildDocument({ title: "标题 A" })]); const preferredData = buildSidebarData([ buildDocument({ title: "标题 B", updated_at: "2026-04-21T00:00:01.000Z", }), ]); const sidebarQuery = { data: preferredData, isLoading: false, error: null, refetch: vi.fn(async () => preferredData), source: "convex-live" as const, }; const treeStream = { data: preferredData, status: "live" as const, cursor: "evt-1", error: null, }; mockUseSidebarData.mockReturnValue(sidebarQuery); mockUseSidebarTreeStream.mockReturnValue(treeStream); mockUsePreferredSidebarSnapshot.mockReturnValue({ data: preferredData, source: "tree_stream", syncKey: "sync-key-1", }); await act(async () => { root.render(
正文
, ); }); expect(capturedSidebarProps).toMatchObject({ initialData, sidebarQuery, sidebarData: preferredData, treeStream, }); expect(capturedBreadcrumbProps).toMatchObject({ documents: preferredData.documents, }); expect(capturedSearchPaletteProps).toMatchObject({ workspaceId: "ws-1", }); }); it("应把同一份 preferred sidebar snapshot 继续提供给页面 children", async () => { const initialData = buildSidebarData([buildDocument({ title: "标题 A" })]); const preferredData = buildSidebarData([ buildDocument({ title: "标题 B", updated_at: "2026-04-21T00:00:01.000Z", }), ]); mockUseSidebarData.mockReturnValue({ data: preferredData, isLoading: false, error: null, refetch: vi.fn(async () => preferredData), source: "convex-live" as const, }); mockUseSidebarTreeStream.mockReturnValue({ data: preferredData, status: "live" as const, cursor: "evt-1", error: null, }); mockUsePreferredSidebarSnapshot.mockReturnValue({ data: preferredData, source: "tree_stream", syncKey: "sync-key-2", }); await act(async () => { root.render( , ); }); const probe = container.querySelector("[data-testid='snapshot-probe']"); expect(probe?.textContent).toBe("标题 B"); }); });