import { act, useEffect } 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 { buildSidebarInitialData } from "@/lib/sidebar-data"; import type { DocumentRecord } from "@/lib/documents"; import { usePreferredSidebarSnapshot } from "./use-preferred-sidebar-snapshot"; 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 Harness(props: { initialData: SidebarInitialData; sidebarQueryData: SidebarInitialData | null; treeStreamData: SidebarInitialData | null; treeStreamStatus?: "idle" | "connecting" | "live" | "fallback"; onState: (state: ReturnType) => void; }) { const state = usePreferredSidebarSnapshot(props); useEffect(() => { props.onState(state); }, [props, state]); return null; } describe("usePreferredSidebarSnapshot", () => { let container: HTMLDivElement; let root: Root; const onState = vi.fn(); 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); onState.mockClear(); }); afterEach(() => { act(() => { root.unmount(); }); container.remove(); }); it("tree stream 仍停在 initial 时,query 新快照不应被旧 stream 压住", async () => { const initialData = buildSidebarData([buildDocument({ title: "标题 A" })]); const staleTreeStream = buildSidebarData([buildDocument({ title: "标题 A" })]); const refreshedQuery = buildSidebarData([ buildDocument({ title: "标题 B", updated_at: "2026-04-21T00:00:01.000Z", }), ]); const caughtUpTreeStream = buildSidebarData([ buildDocument({ title: "标题 B", updated_at: "2026-04-21T00:00:01.000Z", }), ]); await act(async () => { root.render( , ); }); expect(onState.mock.lastCall?.[0]).toMatchObject({ source: "tree_stream", data: expect.objectContaining({ kernelSidebarTree: [expect.objectContaining({ title: "标题 A" })], }), }); await act(async () => { root.render( , ); }); expect(onState.mock.lastCall?.[0]).toMatchObject({ source: "query", data: expect.objectContaining({ kernelSidebarTree: [expect.objectContaining({ title: "标题 B" })], }), }); await act(async () => { root.render( , ); }); expect(onState.mock.lastCall?.[0]).toMatchObject({ source: "tree_stream", data: expect.objectContaining({ kernelSidebarTree: [expect.objectContaining({ title: "标题 B" })], }), }); }); it("tree stream 进入 fallback 后应回退到 query 快照", async () => { const initialData = buildSidebarData([buildDocument({ title: "标题 A" })]); const staleTreeStream = buildSidebarData([buildDocument({ title: "标题 A" })]); const refreshedQuery = buildSidebarData([ buildDocument({ title: "标题 B", updated_at: "2026-04-21T00:00:01.000Z", }), ]); const caughtUpTreeStream = buildSidebarData([ buildDocument({ title: "标题 B", updated_at: "2026-04-21T00:00:01.000Z", }), ]); await act(async () => { root.render( , ); }); expect(onState.mock.lastCall?.[0]).toMatchObject({ source: "query", data: expect.objectContaining({ kernelSidebarTree: [expect.objectContaining({ title: "标题 B" })], }), }); await act(async () => { root.render( , ); }); expect(onState.mock.lastCall?.[0]).toMatchObject({ source: "tree_stream", data: expect.objectContaining({ kernelSidebarTree: [expect.objectContaining({ title: "标题 B" })], }), }); }); it("fallback 且 query 不可用时应回到 initial 快照,而不是继续复用旧 stream 数据", async () => { const initialData = buildSidebarData([buildDocument({ title: "初始标题" })]); const staleTreeStream = buildSidebarData([ buildDocument({ title: "旧 stream 标题", updated_at: "2026-04-21T00:00:01.000Z", }), ]); await act(async () => { root.render( , ); }); await act(async () => { root.render( , ); }); expect(onState.mock.lastCall?.[0]).toMatchObject({ source: "initial", data: expect.objectContaining({ kernelSidebarTree: [expect.objectContaining({ title: "初始标题" })], }), }); }); });