Files
mnote/wolai-frontend/src/components/app-layout-shell.test.tsx
T
lix-2026 8353aea2f9 feat(editor): save leptos island and page aggregate alignment progress
- 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
2026-04-22 05:57:06 +08:00

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");
});
});