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
@@ -72,6 +72,27 @@ function fileUrlToPath(value) {
return decodeURIComponent(url.pathname);
}
function cssEscape(value) {
return String(value).replace(/["\\]/g, "\\$&");
}
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 signUp(context, baseUrl, actorId) {
const response = await context.request.fetch(`${baseUrl}/api/auth`, {
method: "POST",
@@ -163,8 +184,28 @@ async function main() {
const context = await browser.newContext({ viewport: { width: 1360, height: 920 } });
const page = await context.newPage();
const diagnostics = [];
const viewStateRequests = [];
page.on("console", (message) => diagnostics.push(`console:${message.type()}:${message.text()}`));
page.on("pageerror", (error) => diagnostics.push(`pageerror:${error.message}`));
page.on("request", (request) => {
if (!request.url().includes("/api/tree/view-state")) return;
viewStateRequests.push({
method: request.method(),
url: request.url(),
postData: request.postData() || "",
at: Date.now(),
});
});
page.on("response", async (response) => {
if (!response.url().includes("/api/tree/view-state")) return;
const request = response.request();
const entry = viewStateRequests.findLast((candidate) => candidate.url === request.url() && candidate.method === request.method() && !candidate.status);
if (!entry) return;
entry.status = response.status();
entry.doneAt = Date.now();
entry.durationMs = entry.doneAt - entry.at;
entry.body = await response.text().catch(() => "");
});
try {
await waitForHttpOk(`${baseUrl}/health`, 60_000);
@@ -182,6 +223,17 @@ async function main() {
fileTreeUrl.searchParams.set("rootUri", rootUri);
fileTreeUrl.searchParams.set("treeView", "filetree");
await page.goto(fileTreeUrl.toString(), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.locator('[data-mnote-sidebar-tree-tab="page"]').click({ timeout: UI_TIMEOUT_MS });
const rootPageTreePut = 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":"root"/.test(postData);
}, { timeout: UI_TIMEOUT_MS });
await setPageRowExpanded(page, "local-dir:design", false);
const rootPageTreeResponse = await rootPageTreePut;
assert(rootPageTreeResponse.ok(), `root PageTree view-state 保存失败: ${rootPageTreeResponse.status()}`);
await page.locator('[data-mnote-sidebar-tree-tab="filetree"]').click({ timeout: UI_TIMEOUT_MS });
const designRow = page.locator('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path="design"]').first();
await designRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
@@ -220,6 +272,14 @@ async function main() {
const pageTreeText = await page.locator("#sidebar-tree-root").innerText({ timeout: UI_TIMEOUT_MS });
assert.match(pageTreeText, /Brief/, `星标 design 后页面树应显示 design 内 Markdown: ${pageTreeText}`);
assert.doesNotMatch(pageTreeText, /Home|Other/, `星标 design 后页面树不应显示根目录 Markdown: ${pageTreeText}`);
assert(
viewStateRequests.some((entry) => entry.method === "PUT" && entry.status === 200 && /"treeKind":"pagetree"/.test(entry.postData) && /"scope":"root"/.test(entry.postData)),
`应记录 root PageTree view-state PUT: ${JSON.stringify(viewStateRequests)}`,
);
assert(
viewStateRequests.some((entry) => entry.method === "GET" && entry.status === 200 && entry.url.includes("treeKind=pagetree") && entry.url.includes("scope=design")),
`星标 design 应读取 scoped PageTree view-state: ${JSON.stringify(viewStateRequests)}`,
);
await openLocalDocument(page, baseUrl, rootUri, workspaceId, "local-md:Home.md");
const editor = page.locator(".document-pane[data-pane-role='primary'] .editor-surface .ProseMirror[contenteditable='true']").first();
@@ -258,7 +318,7 @@ async function main() {
const reloadedText = await readEditorText(page);
assert.match(reloadedText, /111/, `切换页面再回 Home.md 后编辑内容应保留: ${reloadedText}`);
console.log(JSON.stringify({ ok: true, task: TASK, baseUrl }, null, 2));
console.log(JSON.stringify({ ok: true, task: TASK, baseUrl, viewStateRequests }, null, 2));
} finally {
await browser.close();
server.kill("SIGINT");