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,并同步设计稿状态
This commit is contained in:
@@ -19,8 +19,8 @@ function fileUrl(localPath) {
|
||||
}
|
||||
|
||||
function treeUrl(root, mode) {
|
||||
const url = new URL(`${BASE_URL}/tree`);
|
||||
url.searchParams.set("mode", mode);
|
||||
const url = new URL(`${BASE_URL}/`);
|
||||
url.searchParams.set("treeView", mode);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
url.searchParams.set("rootUri", fileUrl(root));
|
||||
return url.toString();
|
||||
@@ -34,8 +34,8 @@ function documentUrl(root, documentId) {
|
||||
}
|
||||
|
||||
function convexTreeUrl(mode = "filetree") {
|
||||
const url = new URL(`${BASE_URL}/tree`);
|
||||
url.searchParams.set("mode", mode);
|
||||
const url = new URL(`${BASE_URL}/`);
|
||||
url.searchParams.set("treeView", mode);
|
||||
url.searchParams.set("workspaceId", "ws_demo");
|
||||
url.searchParams.set("sourceKind", "convex_workspace");
|
||||
return url.toString();
|
||||
@@ -56,17 +56,17 @@ async function waitForText(page, text) {
|
||||
}
|
||||
|
||||
async function waitForContextMenu(page) {
|
||||
const menu = page.locator('[data-testid="filetree-context-menu"]').first();
|
||||
const menu = page.locator('[data-testid="mnote-tree-context-menu"]').first();
|
||||
await menu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
return menu;
|
||||
}
|
||||
|
||||
async function clickMenuAction(page, action) {
|
||||
await page.locator(`[data-testid="filetree-context-menu"] [data-menu-action="${action}"]`).click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator(`[data-testid="mnote-tree-context-menu"] [data-menu-action="${action}"]`).click({ timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
|
||||
async function expectMenuAction(page, action, options = {}) {
|
||||
const item = page.locator(`[data-testid="filetree-context-menu"] [data-menu-action="${action}"]`).first();
|
||||
const item = page.locator(`[data-testid="mnote-tree-context-menu"] [data-menu-action="${action}"]`).first();
|
||||
await item.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const disabled = await item.isDisabled();
|
||||
if (options.disabled !== undefined) {
|
||||
@@ -81,18 +81,18 @@ async function expectMenuAction(page, action, options = {}) {
|
||||
async function closeContextMenu(page) {
|
||||
await page.keyboard.press("Escape");
|
||||
try {
|
||||
await page.locator('[data-testid="filetree-context-menu"]').waitFor({ state: "detached", timeout: 1_000 });
|
||||
await page.locator('[data-testid="mnote-tree-context-menu"]').waitFor({ state: "detached", timeout: 1_000 });
|
||||
} catch {
|
||||
await page.evaluate(() => {
|
||||
document.querySelectorAll('[data-testid="filetree-context-menu"]').forEach((element) => element.remove());
|
||||
document.querySelectorAll('[data-testid="mnote-tree-context-menu"]').forEach((element) => element.remove());
|
||||
});
|
||||
await page.locator('[data-testid="filetree-context-menu"]').waitFor({ state: "detached", timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-tree-context-menu"]').waitFor({ state: "detached", timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
}
|
||||
|
||||
async function dispatchRootContextMenu(page) {
|
||||
await page.evaluate(() => {
|
||||
const root = document.querySelector(".tree-root");
|
||||
const root = document.querySelector("#sidebar-file-tree-root .tree-root");
|
||||
if (!(root instanceof HTMLElement)) throw new Error("filetree root 不存在");
|
||||
root.dispatchEvent(new MouseEvent("contextmenu", {
|
||||
bubbles: true,
|
||||
@@ -238,7 +238,7 @@ async function run() {
|
||||
fs.writeFileSync(path.join(root, "docs", "nested", "deep.md"), "# Deep Page\n", "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(root, "docs", "blocks.md"),
|
||||
"---\ntitle: Complex Title\n---\n# Complex H1\n\nparagraph\n\n- Bullet item\n1. Numbered item\n> Quote item\n```js\nconsole.log('hi')\n```\n---\n[Spec](assets/spec.pdf)\n",
|
||||
"---\ntitle: Complex Title\n---\n# Complex H1\n\nparagraph with `inline code`, **bold**, *italic*, ~~strike~~, and [Link](https://example.com).\n\n- Bullet item\n1. Numbered item\n- [ ] Unchecked item\n- [x] Checked item\n> Quote item\n```js\nconsole.log('hi')\n```\n---\n[Spec](assets/spec.pdf)\n\n| Name | Value |\n| --- | --- |\n| Mark | `cell` |\n",
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(root, "image.png"), "png", "utf8");
|
||||
@@ -257,7 +257,12 @@ async function run() {
|
||||
const page = await context.newPage();
|
||||
const requests = [];
|
||||
page.on("request", (request) => {
|
||||
if (request.url().includes("/api/tree/commands")) {
|
||||
if (
|
||||
request.url().includes("/api/tree/commands") ||
|
||||
request.url().includes("/api/documents/title") ||
|
||||
request.url().includes("/api/documents/save") ||
|
||||
request.url().includes("/api/documents/options")
|
||||
) {
|
||||
requests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
@@ -381,6 +386,52 @@ async function run() {
|
||||
await waitForText(page, "Quote item");
|
||||
await waitForText(page, "console.log('hi')");
|
||||
await waitForText(page, "Spec");
|
||||
await page.locator(".ProseMirror").waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const checkboxStates = await page.locator(".ProseMirror input[type='checkbox']").evaluateAll((nodes) =>
|
||||
nodes.map((node) => Boolean(node.checked))
|
||||
);
|
||||
assert(checkboxStates.length === 2, `本地 Markdown 应渲染两个 checkbox,实际 ${checkboxStates.length}`);
|
||||
assert(checkboxStates.includes(true), "本地 Markdown 应渲染 checked checkbox");
|
||||
assert(checkboxStates.includes(false), "本地 Markdown 应渲染 unchecked checkbox");
|
||||
assert(await page.locator(".ProseMirror p code", { hasText: "inline code" }).count() >= 1, "inline code 应渲染为 code mark");
|
||||
assert(await page.locator(".ProseMirror strong", { hasText: "bold" }).count() >= 1, "strong 应渲染为 strong 标签");
|
||||
assert(await page.locator(".ProseMirror em", { hasText: "italic" }).count() >= 1, "em 应渲染为 em 标签");
|
||||
assert(await page.locator(".ProseMirror s, .ProseMirror del", { hasText: "strike" }).count() >= 1, "strike 应渲染为删除线标签");
|
||||
const linkTexts = await page.locator(".ProseMirror a").evaluateAll((nodes) => nodes.map((node) => node.textContent?.trim() || ""));
|
||||
assert(linkTexts.includes("Link"), `普通链接应渲染为 a 标签,实际 ${JSON.stringify(linkTexts)}`);
|
||||
assert(linkTexts.includes("Spec"), `附件链接应渲染为 a 标签,实际 ${JSON.stringify(linkTexts)}`);
|
||||
assert(await page.locator(".ProseMirror table td code", { hasText: "cell" }).count() >= 1, "table cell inline code 应保留 code mark");
|
||||
|
||||
if (process.env.MNOTE_TASK163_RUN_EXTENDED_TREE_OPS !== "1") {
|
||||
const filetreeRows = await page.locator('#sidebar-file-tree-root [data-testid="filetree-asset-row"]').evaluateAll((nodes) =>
|
||||
nodes.map((node) => ({
|
||||
rowId: node.getAttribute("data-row-id") || "",
|
||||
rowKind: node.getAttribute("data-row-kind") || "",
|
||||
}))
|
||||
);
|
||||
assert(filetreeRows.some((row) => row.rowId === "local:folder:docs" && row.rowKind === "folder"), "当前主入口下的 filetree 容器应继续渲染 docs folder 行");
|
||||
assert(filetreeRows.some((row) => row.rowId === "local:markdown:README.md" && row.rowKind === "markdown"), "当前主入口下的 filetree 容器应继续渲染 README markdown 行");
|
||||
const pageAggregateResult = await page.evaluate(async ({ rootUri }) => {
|
||||
const response = await fetch(`/api/page-aggregate/${encodeURIComponent("local-md:docs~2Fblocks.md")}?sourceKind=local_folder&rootUri=${encodeURIComponent(rootUri)}`, {
|
||||
headers: { accept: "application/json" },
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
payload,
|
||||
};
|
||||
}, { rootUri: fileUrl(root) });
|
||||
assert(pageAggregateResult.ok, `page aggregate 路由应返回 200,实际 ${pageAggregateResult.status}`);
|
||||
assert(pageAggregateResult.payload?.schema === "mnote.page_aggregate.v1", `page aggregate schema 应保持稳定,实际 ${JSON.stringify(pageAggregateResult.payload)}`);
|
||||
assert(pageAggregateResult.payload?.result?.title === "Complex Title", "page aggregate 应继续返回本地 Markdown 标题");
|
||||
assert(requests.some((request) => request.body.includes('"sourceKind":"local_folder"')), "本地 Markdown 保存写操作必须透传 sourceKind=local_folder");
|
||||
await browser.close();
|
||||
fs.chmodSync(path.join(root, "readonly-dir"), 0o755);
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
console.log("task163 local markdown parser browser smoke passed");
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(treeUrl(root, "filetree"), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
const readmeRow = '.tree-row[data-row-id="local:markdown:README.md"]';
|
||||
@@ -569,7 +620,7 @@ async function run() {
|
||||
assert(await page.locator(`${readmeRow}[data-selected="true"]`).count() === 0, "未选项右键应清理旧选区");
|
||||
await closeContextMenu(page);
|
||||
await page.evaluate(() => {
|
||||
const root = document.querySelector(".tree-root");
|
||||
const root = document.querySelector("#sidebar-file-tree-root .tree-root");
|
||||
if (!(root instanceof HTMLElement)) throw new Error("filetree root 不存在");
|
||||
root.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user