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,144 @@
|
||||
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> = {}): 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;
|
||||
treeStreamData: SidebarInitialData | null;
|
||||
onState: (state: ReturnType<typeof usePreferredSidebarSnapshot>) => 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 落后于 query refetch 时应优先使用更新后的 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(
|
||||
<Harness
|
||||
initialData={initialData}
|
||||
sidebarQueryData={initialData}
|
||||
treeStreamData={staleTreeStream}
|
||||
onState={onState}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(onState.mock.lastCall?.[0]).toMatchObject({
|
||||
source: "tree_stream",
|
||||
data: expect.objectContaining({
|
||||
kernelSidebarTree: [expect.objectContaining({ title: "标题 A" })],
|
||||
}),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Harness
|
||||
initialData={initialData}
|
||||
sidebarQueryData={refreshedQuery}
|
||||
treeStreamData={staleTreeStream}
|
||||
onState={onState}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(onState.mock.lastCall?.[0]).toMatchObject({
|
||||
source: "query",
|
||||
data: expect.objectContaining({
|
||||
kernelSidebarTree: [expect.objectContaining({ title: "标题 B" })],
|
||||
}),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Harness
|
||||
initialData={initialData}
|
||||
sidebarQueryData={refreshedQuery}
|
||||
treeStreamData={caughtUpTreeStream}
|
||||
onState={onState}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(onState.mock.lastCall?.[0]).toMatchObject({
|
||||
source: "tree_stream",
|
||||
data: expect.objectContaining({
|
||||
kernelSidebarTree: [expect.objectContaining({ title: "标题 B" })],
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user