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,156 @@
|
||||
import { act } from "react";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { PreferredSidebarSnapshotProvider } from "@/components/sidebar/preferred-sidebar-snapshot-context";
|
||||
import type { SidebarInitialData } from "@/components/sidebar/types";
|
||||
import { buildSidebarInitialData } from "@/lib/sidebar-data";
|
||||
import type { DocumentRecord } from "@/lib/documents";
|
||||
import { usePageHeadTitle } from "./use-page-head-title";
|
||||
|
||||
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 HookProbe(props: { documentId: string; fallbackTitle: string }) {
|
||||
const state = usePageHeadTitle(props);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-testid="page-head-title"
|
||||
data-display-title={state.displayTitle}
|
||||
data-committed-title={state.committedTitle}
|
||||
data-has-draft={state.hasDraft ? "1" : "0"}
|
||||
/>
|
||||
<button type="button" data-testid="set-draft" onClick={() => state.setDraftTitle(" 新标题 ")}>
|
||||
设草稿
|
||||
</button>
|
||||
<button type="button" data-testid="commit-persisted" onClick={() => state.commitPersistedTitle("新标题")}>
|
||||
提交持久化
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
describe("usePageHeadTitle", () => {
|
||||
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);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("应优先使用 live sidebar snapshot 中的标题作为 committed title", async () => {
|
||||
const snapshot = buildSidebarData([
|
||||
buildDocument({
|
||||
id: "doc-1",
|
||||
title: "树标题",
|
||||
}),
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<PreferredSidebarSnapshotProvider data={snapshot}>
|
||||
<HookProbe documentId="doc-1" fallbackTitle="SSR 标题" />
|
||||
</PreferredSidebarSnapshotProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
const probe = container.querySelector("[data-testid='page-head-title']");
|
||||
expect(probe?.getAttribute("data-committed-title")).toBe("树标题");
|
||||
expect(probe?.getAttribute("data-display-title")).toBe("树标题");
|
||||
expect(probe?.getAttribute("data-has-draft")).toBe("0");
|
||||
});
|
||||
|
||||
it("应只把本地输入保留为短暂 draft,并在 live 标题追平后自动清空", async () => {
|
||||
const initialSnapshot = buildSidebarData([
|
||||
buildDocument({
|
||||
id: "doc-1",
|
||||
title: "旧标题",
|
||||
}),
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<PreferredSidebarSnapshotProvider data={initialSnapshot}>
|
||||
<HookProbe documentId="doc-1" fallbackTitle="SSR 标题" />
|
||||
</PreferredSidebarSnapshotProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
const setDraftButton = container.querySelector<HTMLButtonElement>("[data-testid='set-draft']");
|
||||
act(() => {
|
||||
setDraftButton?.click();
|
||||
});
|
||||
|
||||
let probe = container.querySelector("[data-testid='page-head-title']");
|
||||
expect(probe?.getAttribute("data-display-title")).toBe(" 新标题 ");
|
||||
expect(probe?.getAttribute("data-committed-title")).toBe("旧标题");
|
||||
expect(probe?.getAttribute("data-has-draft")).toBe("1");
|
||||
|
||||
const commitPersistedButton = container.querySelector<HTMLButtonElement>("[data-testid='commit-persisted']");
|
||||
act(() => {
|
||||
commitPersistedButton?.click();
|
||||
});
|
||||
|
||||
probe = container.querySelector("[data-testid='page-head-title']");
|
||||
expect(probe?.getAttribute("data-display-title")).toBe("新标题");
|
||||
expect(probe?.getAttribute("data-has-draft")).toBe("1");
|
||||
|
||||
const syncedSnapshot = buildSidebarData([
|
||||
buildDocument({
|
||||
id: "doc-1",
|
||||
title: "新标题",
|
||||
updated_at: "2026-04-21T00:00:02.000Z",
|
||||
}),
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<PreferredSidebarSnapshotProvider data={syncedSnapshot}>
|
||||
<HookProbe documentId="doc-1" fallbackTitle="SSR 标题" />
|
||||
</PreferredSidebarSnapshotProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
probe = container.querySelector("[data-testid='page-head-title']");
|
||||
expect(probe?.getAttribute("data-display-title")).toBe("新标题");
|
||||
expect(probe?.getAttribute("data-committed-title")).toBe("新标题");
|
||||
expect(probe?.getAttribute("data-has-draft")).toBe("0");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user