Files
mnote/scripts/task120-rust-web-tree-integration-smoke.js
T
lix-2026 d148e1ccc2 feat: 收口本地文件夹入口并推进 page aggregate rust-first
- 为 Rust Web 主入口补齐本地文件夹/云空间切换、最近目录与路径回填体验\n- 对齐 local markdown media 与 inline marks 的 Rust shell / TipTap converter 语义\n- 让 documents/page 优先消费 Rust page aggregate snapshot,并保留 TS fallback\n- 补强 tree live、local markdown 与主入口 smoke,并同步设计稿状态
2026-05-09 19:05:06 +08:00

254 lines
9.8 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const { chromium } = require("playwright");
const { fetchWithTimeout } = require("./task114-rust-web-gateway-entry-smoke.js");
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
async function readJsonResponse(response, label) {
const text = await response.text();
let payload = null;
try {
payload = text ? JSON.parse(text) : null;
} catch {
throw new Error(`${label} 返回了非 JSON 内容: ${text.slice(0, 1200)}`);
}
assert(response.ok, `${label} 请求失败: ${response.status}; body: ${text.slice(0, 1200)}`);
return payload;
}
async function postTreeCommand(body, label) {
const response = await fetchWithTimeout(`${BASE_URL}/api/tree/commands`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const payload = await readJsonResponse(response, label);
const result = payload && typeof payload.result === "object" ? payload.result : null;
assert(result, `${label} 缺少 result`);
return result;
}
async function createTempPage(title, extra = {}) {
const result = await postTreeCommand(
{
action: "create",
title,
...extra,
},
`创建临时页面 ${title}`,
);
assert(result.documentId, `创建临时页面 ${title} 缺少 documentId`);
assert(result.workspaceId, `创建临时页面 ${title} 缺少 workspaceId`);
return {
documentId: result.documentId,
workspaceId: result.workspaceId,
title,
};
}
async function purgeTempPage(target) {
if (!target?.documentId || !target?.workspaceId) return;
await postTreeCommand(
{
action: "purge",
workspaceId: target.workspaceId,
documentId: target.documentId,
},
`清理临时页面 ${target.documentId}`,
);
}
async function waitForSidebarRow(page, documentId) {
const selector = `[data-testid="wolai-sidebar-row"][data-node-id="${documentId}"]`;
const row = page.locator(selector).first();
await row.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
return row;
}
async function armTreeLiveProbe(page) {
await page.evaluate(() => {
const key = "__task120TreeLiveEvents";
window[key] = [];
if (window.__task120TreeLiveProbeArmed) return;
const push = (kind, detail) => {
const payload = detail && typeof detail === "object" && "payload" in detail ? detail.payload : detail;
window[key].push({
kind,
applied: document.documentElement.getAttribute("data-mnote-tree-live-applied") || "",
payload,
});
if (window[key].length > 20) window[key].shift();
};
window.addEventListener("tree:delta", (event) => push("delta", event.detail));
window.addEventListener("tree:resync", (event) => push("resync", event.detail));
window.__task120TreeLiveProbeArmed = true;
});
}
async function waitForTreeLiveApplied(page, label, expectedTitle) {
await page.waitForFunction(
({ title }) => {
const events = Array.isArray(window.__task120TreeLiveEvents) ? window.__task120TreeLiveEvents : [];
return events.some((event) => {
if (!event || (event.kind !== "delta" && event.kind !== "resync")) return false;
const applied = event.applied || document.documentElement.getAttribute("data-mnote-tree-live-applied") || "";
if (applied !== "delta" && applied !== "resync") return false;
try {
return JSON.stringify(event.payload || {}).includes(title);
} catch {
return false;
}
});
},
{ title: expectedTitle },
{ timeout: UI_TIMEOUT_MS },
);
const probe = await page.evaluate(({ title }) => {
const events = Array.isArray(window.__task120TreeLiveEvents) ? window.__task120TreeLiveEvents : [];
const match = events.find((event) => {
if (!event || (event.kind !== "delta" && event.kind !== "resync")) return false;
try {
return JSON.stringify(event.payload || {}).includes(title);
} catch {
return false;
}
});
return {
applied: document.documentElement.getAttribute("data-mnote-tree-live-applied") || "",
kind: match?.kind || "",
};
}, { title: expectedTitle });
assert(["delta", "resync"].includes(probe.applied || ""), `${label} 应应用 delta/resync,实际: ${probe.applied}`);
assert(["delta", "resync"].includes(probe.kind || ""), `${label} 应捕获匹配 rename 的 delta/resync 事件,实际: ${probe.kind}`);
return probe.kind || probe.applied;
}
async function readTreeSnapshot(workspaceId) {
const response = await fetchWithTimeout(
`${BASE_URL}/api/tree/events?workspaceId=${encodeURIComponent(workspaceId)}&maxPolls=0`,
{ headers: { accept: "text/event-stream" } },
);
const text = await response.text();
assert.equal(response.status, 200, `/api/tree/events 请求失败: ${response.status}; body: ${text.slice(0, 1200)}`);
assert.equal(response.headers.get("x-mnote-web-owner"), "mnote-web");
assert.match(text, /event:\s*snapshot|event:snapshot/, "tree events 未返回 snapshot 事件");
return text;
}
async function main() {
const suffix = Date.now().toString(36);
const rootTitle = `task120-root-${suffix}`;
const childTitle = `task120-child-${suffix}`;
let rootPage = null;
let childPage = null;
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
const page = await context.newPage();
try {
rootPage = await createTempPage(rootTitle);
childPage = await createTempPage(childTitle, {
workspaceId: rootPage.workspaceId,
parentId: rootPage.documentId,
});
const targetUrl = `${BASE_URL}/documents/${encodeURIComponent(rootPage.documentId)}?workspaceId=${encodeURIComponent(rootPage.workspaceId)}`;
const response = await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
assert(response, "文档页没有返回响应");
assert.equal(response.status(), 200, `文档页状态码异常: ${response.status()}`);
assert.equal(response.headers()["x-mnote-web-owner"], "mnote-web", "文档页必须由 mnote-web 拥有");
const rootRow = await waitForSidebarRow(page, rootPage.documentId);
const childRow = await waitForSidebarRow(page, childPage.documentId);
await expectRowTitle(rootRow, rootTitle, "root row 标题");
await expectRowTitle(childRow, childTitle, "child row 标题");
assert.equal(await childRow.getAttribute("data-parent-id"), rootPage.documentId, "子页面 row 缺少真实 parent id");
assert.equal(await rootRow.getAttribute("data-active"), "true", "当前文档 root row 没有 active 标记");
await childRow.click({ timeout: UI_TIMEOUT_MS });
await page.waitForURL((url) => url.pathname === `/documents/${childPage.documentId}`, {
timeout: UI_TIMEOUT_MS,
waitUntil: "domcontentloaded",
});
const activeChildRow = await waitForSidebarRow(page, childPage.documentId);
assert.equal(await activeChildRow.getAttribute("data-active"), "true", "点击 child row 后 active 标记未切换");
const renamedTitle = `${childTitle}-renamed`;
await armTreeLiveProbe(page);
await page.evaluate(() => {
document.documentElement.removeAttribute("data-mnote-tree-live-applied");
window.__task120TreeLiveEvents = [];
});
await postTreeCommand(
{
action: "rename",
workspaceId: childPage.workspaceId,
documentId: childPage.documentId,
title: renamedTitle,
},
"重命名临时子页面",
);
const liveApplied = await waitForTreeLiveApplied(page, "重命名后 tree live", renamedTitle);
const renamedLiveRow = await waitForSidebarRow(page, childPage.documentId);
await expectRowTitle(renamedLiveRow, renamedTitle, `tree live ${liveApplied} 后 child row 标题`);
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
const renamedRow = await waitForSidebarRow(page, childPage.documentId);
await expectRowTitle(renamedRow, renamedTitle, "重命名后 child row 标题");
const snapshotText = await readTreeSnapshot(rootPage.workspaceId);
assert(snapshotText.includes(rootPage.documentId), "tree events snapshot 未包含 root 页面");
assert(snapshotText.includes(childPage.documentId), "tree events snapshot 未包含 child 页面");
const purgedChild = childPage;
childPage = null;
await purgeTempPage(purgedChild);
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
(documentId) => !document.querySelector(`[data-testid="wolai-sidebar-row"][data-node-id="${documentId}"]`),
purgedChild.documentId,
{ timeout: UI_TIMEOUT_MS },
);
console.log(
JSON.stringify(
{
ok: true,
baseUrl: BASE_URL,
workspaceId: rootPage.workspaceId,
rootDocumentId: rootPage.documentId,
childDocumentId: purgedChild.documentId,
},
null,
2,
),
);
} finally {
if (childPage) {
await purgeTempPage(childPage).catch(() => undefined);
}
if (rootPage) {
await purgeTempPage(rootPage).catch(() => undefined);
}
await page.close().catch(() => undefined);
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
}
async function expectRowTitle(row, expected, label) {
const text = (await row.innerText({ timeout: UI_TIMEOUT_MS })).trim();
assert(text.includes(expected), `${label} 不匹配: expected=${expected}; actual=${text}`);
}
if (require.main === module) {
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});
}