- switch main document flow toward leptos tiptap island host and generated runtime assets - align page aggregate loading, page head single-source updates, and AI tool result recovery - add tests and smoke scripts for title sync, AI route recovery, and editor host cutover
206 lines
6.0 KiB
TypeScript
206 lines
6.0 KiB
TypeScript
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<string, unknown> | null = null;
|
|
let capturedBreadcrumbProps: Record<string, unknown> | null = null;
|
|
let capturedSearchPaletteProps: Record<string, unknown> | 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<string, unknown>) => {
|
|
capturedSidebarProps = props;
|
|
return <div data-testid="sidebar" />;
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/breadcrumb", () => ({
|
|
Breadcrumb: (props: Record<string, unknown>) => {
|
|
capturedBreadcrumbProps = props;
|
|
return <div data-testid="breadcrumb" />;
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/mobile-sidebar-trigger", () => ({
|
|
MobileSidebarTrigger: () => <div data-testid="mobile-trigger" />,
|
|
}));
|
|
|
|
vi.mock("@/components/search/search-palette", () => ({
|
|
SearchPalette: (props: Record<string, unknown>) => {
|
|
capturedSearchPaletteProps = props;
|
|
return <div data-testid="search-palette" />;
|
|
},
|
|
}));
|
|
|
|
function buildDocument(overrides: Partial<DocumentRecord> = {}): 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 <div data-testid="snapshot-probe">{snapshot.documents[0]?.title ?? "空"}</div>;
|
|
}
|
|
|
|
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(
|
|
<AppLayoutShell initialData={initialData}>
|
|
<div>正文</div>
|
|
</AppLayoutShell>,
|
|
);
|
|
});
|
|
|
|
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(
|
|
<AppLayoutShell initialData={initialData}>
|
|
<SnapshotProbe />
|
|
</AppLayoutShell>,
|
|
);
|
|
});
|
|
|
|
const probe = container.querySelector("[data-testid='snapshot-probe']");
|
|
expect(probe?.textContent).toBe("标题 B");
|
|
});
|
|
});
|