Implement sidebar tree view state persistence

This commit is contained in:
lix-2026
2026-05-27 16:32:05 +08:00
parent bd7470d03e
commit e43efacbce
15 changed files with 1875 additions and 17 deletions
@@ -74,6 +74,10 @@ function fileUrlToPath(value) {
return decodeURIComponent(url.pathname);
}
function cssEscape(value) {
return String(value).replace(/["\\]/g, "\\$&");
}
async function signUp(context, baseUrl, actorId) {
const authResponse = await context.request.fetch(`${baseUrl}/api/auth`, {
method: "POST",
@@ -126,6 +130,23 @@ async function expandFolder(page, relativePath) {
}
}
async function setPageRowExpanded(page, nodeId, expanded) {
const selector = `#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${cssEscape(nodeId)}"]`;
await page.locator(selector).waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const current = await page.locator(selector).first().getAttribute("aria-expanded");
if ((current === "true") !== expanded) {
await page.locator(`${selector} [data-rust-action="toggle"]`).first().click({ timeout: UI_TIMEOUT_MS });
}
await page.waitForFunction(
({ targetNodeId, expected }) => {
const row = document.querySelector(`#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${CSS.escape(targetNodeId)}"]`);
return row && row.getAttribute("aria-expanded") === String(expected);
},
{ targetNodeId: nodeId, expected: expanded },
{ timeout: UI_TIMEOUT_MS },
);
}
async function main() {
fs.mkdirSync(RESULT_DIR, { recursive: true });
const port = await pickPort();
@@ -219,6 +240,7 @@ async function main() {
const url = request.url();
if (
url.includes("/api/tree/projections/")
|| url.includes("/api/tree/view-state")
|| url.includes("/api/page-aggregate/")
|| url.includes("/api/leptos-tiptap-runtime/")
|| url.includes("/documents/")
@@ -329,6 +351,49 @@ async function main() {
const staleRowsAfterStarredOpen = await page.locator('#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="stale-my-space-page"]').count();
assert.equal(staleRowsAfterStarredOpen, 0, "星标 design 打开后旧我的空间页面树不应长期残留");
await page.locator('[data-mnote-sidebar-tree-tab="page"]').click({ timeout: UI_TIMEOUT_MS });
const pageStatePut = page.waitForResponse((response) => {
const postData = response.request().postData() || "";
return response.url().includes("/api/tree/view-state")
&& response.request().method() === "PUT"
&& /"treeKind":"pagetree"/.test(postData)
&& /"scope":"design"/.test(postData);
}, { timeout: UI_TIMEOUT_MS });
const collapsedPageNodeId = "local-dir:design~2F05-editor-mainline~2Fprocess";
const expandedPageNodeId = "local-dir:design~2F05-editor-mainline~2Freference";
await setPageRowExpanded(page, expandedPageNodeId, false);
await setPageRowExpanded(page, collapsedPageNodeId, false);
await setPageRowExpanded(page, expandedPageNodeId, true);
const pageStateResponse = await pageStatePut;
assert(pageStateResponse.ok(), `PageTree scoped view-state 保存失败: ${pageStateResponse.status()}`);
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.locator('[data-mnote-sidebar-tree-tab="page"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
({ collapsedPageNodeId, expandedPageNodeId }) => {
const collapsed = document.querySelector(`#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${CSS.escape(collapsedPageNodeId)}"]`);
const expanded = document.querySelector(`#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${CSS.escape(expandedPageNodeId)}"]`);
return collapsed && expanded
&& collapsed.getAttribute("aria-expanded") === "false"
&& expanded.getAttribute("aria-expanded") === "true";
},
{ collapsedPageNodeId, expandedPageNodeId },
{ timeout: UI_TIMEOUT_MS },
).catch(async (error) => {
const state = await page.evaluate(() => ({
href: location.href,
pageApplied: document.documentElement.getAttribute("data-mnote-page-tree-view-state-applied") || "",
viewStateSaved: document.documentElement.getAttribute("data-mnote-sidebar-tree-view-state-saved") || "",
pageTree: Array.from(document.querySelectorAll("#sidebar-tree-root .tree-row[data-shell-mode='page']")).map((row) => ({
nodeId: row.getAttribute("data-node-id") || "",
expanded: row.getAttribute("aria-expanded") || "",
text: row.textContent || "",
})),
}));
throw new Error(`${error.message}; page_tree_state=${JSON.stringify(state)}; requestTimings=${JSON.stringify(requestTimings.filter((entry) => entry.url.includes("/api/tree/view-state")))}`);
});
timeline.pageTreeUserStateRestoredAt = Date.now();
await page.locator('[data-mnote-sidebar-tree-tab="filetree"]').click({ timeout: UI_TIMEOUT_MS });
await page.screenshot({ path: path.join(RESULT_DIR, "after-starred-design.png"), fullPage: true });
timeline.filetreeInteractiveAt = Date.now();
await expandFolder(page, "design/05-editor-mainline");