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
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user