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:
@@ -5,7 +5,7 @@ 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_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
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) {
|
||||
@@ -69,6 +69,65 @@ async function waitForSidebarRow(page, documentId) {
|
||||
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`,
|
||||
@@ -120,6 +179,11 @@ async function main() {
|
||||
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",
|
||||
@@ -129,6 +193,9 @@ async function main() {
|
||||
},
|
||||
"重命名临时子页面",
|
||||
);
|
||||
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 标题");
|
||||
|
||||
@@ -2,33 +2,85 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { fetchWithTimeout } = require("./task114-rust-web-gateway-entry-smoke.js");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/$/, "");
|
||||
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
|
||||
async function fetchText(path) {
|
||||
const response = await fetch(`${BASE_URL}${path}`);
|
||||
const response = await fetchWithTimeout(`${BASE_URL}${path}`);
|
||||
const text = await response.text();
|
||||
assert.equal(response.status, 200, `${path} 请求失败: ${response.status} ${text.slice(0, 200)}`);
|
||||
return { response, text };
|
||||
}
|
||||
|
||||
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) {
|
||||
const result = await postTreeCommand({ action: "create", title }, `创建临时页面 ${title}`);
|
||||
assert(result.documentId, `创建临时页面 ${title} 缺少 documentId`);
|
||||
assert(result.workspaceId, `创建临时页面 ${title} 缺少 workspaceId`);
|
||||
return {
|
||||
documentId: result.documentId,
|
||||
workspaceId: result.workspaceId,
|
||||
};
|
||||
}
|
||||
|
||||
async function purgeTempPage(target) {
|
||||
if (!target?.documentId || !target?.workspaceId) return;
|
||||
await postTreeCommand(
|
||||
{
|
||||
action: "purge",
|
||||
workspaceId: target.workspaceId,
|
||||
documentId: target.documentId,
|
||||
},
|
||||
`清理临时页面 ${target.documentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const page = await fetchText("/documents/doc_1?workspaceId=ws_demo");
|
||||
assert.match(page.text, /__MNOTE_TREE_LIVE_BOOTSTRAP__/);
|
||||
assert.match(page.text, /mnote\.tree_live_bootstrap\.v1/);
|
||||
assert.match(page.text, /\/api\/tree\/events/);
|
||||
assert.match(page.text, /tree:snapshot/);
|
||||
assert.match(page.text, /tree:delta/);
|
||||
assert.match(page.text, /tree:resync/);
|
||||
const pageTarget = await createTempPage(`task123-live-${Date.now().toString(36)}`);
|
||||
try {
|
||||
const page = await fetchText(`/documents/${encodeURIComponent(pageTarget.documentId)}?workspaceId=${encodeURIComponent(pageTarget.workspaceId)}`);
|
||||
assert.match(page.text, /__MNOTE_TREE_LIVE_BOOTSTRAP__/);
|
||||
assert.match(page.text, /mnote\.tree_live_bootstrap\.v1/);
|
||||
assert.match(page.text, /\/api\/tree\/events/);
|
||||
assert.match(page.text, /tree:snapshot/);
|
||||
assert.match(page.text, /tree:delta/);
|
||||
assert.match(page.text, /tree:resync/);
|
||||
|
||||
const events = await fetchText("/api/tree/events?workspaceId=ws_demo&maxPolls=0");
|
||||
assert.match(events.response.headers.get("content-type") || "", /text\/event-stream/);
|
||||
assert.equal(events.response.headers.get("x-mnote-tree-stream-owner"), "rust-web");
|
||||
assert.match(events.text, /event:\s*snapshot/);
|
||||
assert.match(events.text, /id:\s*/);
|
||||
assert.match(events.text, /"revision"/);
|
||||
const events = await fetchText(`/api/tree/events?workspaceId=${encodeURIComponent(pageTarget.workspaceId)}&maxPolls=0`);
|
||||
assert.match(events.response.headers.get("content-type") || "", /text\/event-stream/);
|
||||
assert.equal(events.response.headers.get("x-mnote-tree-stream-owner"), "rust-web");
|
||||
assert.match(events.text, /event:\s*snapshot/);
|
||||
assert.match(events.text, /id:\s*/);
|
||||
assert.match(events.text, /"revision"/);
|
||||
|
||||
console.log(JSON.stringify({ ok: true, owner: "rust-web", stream: "/api/tree/events" }, null, 2));
|
||||
console.log(JSON.stringify({ ok: true, owner: "rust-web", stream: "/api/tree/events", documentId: pageTarget.documentId }, null, 2));
|
||||
} finally {
|
||||
await purgeTempPage(pageTarget).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -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 }));
|
||||
});
|
||||
|
||||
@@ -17,10 +17,12 @@ function fileUrl(filePath) {
|
||||
|
||||
async function main() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-main-local-folder-"));
|
||||
const otherRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-main 本地 #other-"));
|
||||
fs.mkdirSync(path.join(root, "docs"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "README.md"), "# Root Page\n", "utf8");
|
||||
fs.writeFileSync(path.join(root, "docs", "child.md"), "# Child Page\n", "utf8");
|
||||
fs.writeFileSync(path.join(root, "plain.txt"), "plain asset\n", "utf8");
|
||||
fs.writeFileSync(path.join(otherRoot, "OTHER.md"), "# Other Root\n", "utf8");
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
@@ -54,6 +56,11 @@ async function main() {
|
||||
url.searchParams.get("rootUri") === fileUrl(root) &&
|
||||
url.searchParams.get("treeView") === "filetree";
|
||||
}, { timeout: UI_TIMEOUT_MS });
|
||||
const recentLocalRoots = await page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem("mnote.localFolder.recentRoots") || "[]";
|
||||
return JSON.parse(raw);
|
||||
});
|
||||
assert.equal(recentLocalRoots[0], fileUrl(root), "打开本地文件夹后应记录最近 rootUri");
|
||||
|
||||
const bodyText = await page.locator("body").innerText({ timeout: UI_TIMEOUT_MS });
|
||||
assert(!bodyText.includes("legacy_next_compat_disabled"), "本地文件夹主入口不能落入 legacy Next fallback");
|
||||
@@ -83,10 +90,57 @@ async function main() {
|
||||
`Shift+Click 应形成文件树范围多选,实际选中: ${selectedRowIds.join(", ")}`,
|
||||
);
|
||||
|
||||
await page.locator('[data-testid="mnote-workspace-source-trigger"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-workspace-source-menu"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.locator(`[data-testid="mnote-recent-local-root"][data-root-uri="${fileUrl(root)}"]`).waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.locator('[data-testid="mnote-open-other-local-folder"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-local-folder-dialog"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-local-folder-path-input"]').fill(otherRoot);
|
||||
await page.locator('[data-testid="mnote-local-folder-open-confirm"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname === "/" &&
|
||||
url.searchParams.get("sourceKind") === "local_folder" &&
|
||||
url.searchParams.get("rootUri") === fileUrl(otherRoot) &&
|
||||
url.searchParams.get("treeView") === "filetree";
|
||||
}, { timeout: UI_TIMEOUT_MS });
|
||||
const updatedRecentLocalRoots = await page.evaluate(() => {
|
||||
const raw = window.localStorage.getItem("mnote.localFolder.recentRoots") || "[]";
|
||||
return JSON.parse(raw);
|
||||
});
|
||||
assert.equal(updatedRecentLocalRoots[0], fileUrl(otherRoot), "切到其他本地文件夹后应把它放到最近目录首位");
|
||||
assert.equal(updatedRecentLocalRoots[1], fileUrl(root), "之前打开过的本地目录应保留在最近目录列表中");
|
||||
await page.locator('.tree-row[data-row-id="local:markdown:OTHER.md"]').waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
await page.locator('[data-testid="mnote-workspace-source-trigger"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-workspace-source-menu"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-open-other-local-folder"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-local-folder-dialog"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const recentPathInputValue = await page.locator('[data-testid="mnote-local-folder-path-input"]').inputValue({
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
assert.equal(recentPathInputValue, otherRoot, "最近目录回填输入框时应解码 file:// rootUri,避免二次编码");
|
||||
await page.getByRole("button", { name: "取消" }).click({ timeout: UI_TIMEOUT_MS });
|
||||
|
||||
await page.locator('[data-testid="mnote-workspace-source-trigger"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-workspace-source-menu"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('[data-testid="mnote-switch-cloud-workspace"]').click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname === "/" &&
|
||||
url.searchParams.get("sourceKind") === "convex_workspace" &&
|
||||
!url.searchParams.has("rootUri");
|
||||
}, { timeout: UI_TIMEOUT_MS });
|
||||
|
||||
console.log("task164 desktop hot local folder main entry smoke passed");
|
||||
} finally {
|
||||
await browser.close();
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
fs.rmSync(otherRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user