fix local folder resource lifecycle
This commit is contained in:
@@ -0,0 +1,621 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||||
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
|
||||
.find((candidate) => fs.existsSync(candidate));
|
||||
|
||||
function fileUrl(localPath) {
|
||||
return `file://${localPath}`;
|
||||
}
|
||||
|
||||
function localMdDocumentId(relativePath) {
|
||||
return `local-md:${relativePath.replaceAll("/", "~2F")}`;
|
||||
}
|
||||
|
||||
function documentUrl(root, relativePath) {
|
||||
const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
url.searchParams.set("rootUri", fileUrl(root));
|
||||
url.searchParams.set("treeView", "filetree");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function localFolderHomeUrl(root) {
|
||||
const url = new URL(`${BASE_URL}/`);
|
||||
url.searchParams.set("sourceKind", "local_folder");
|
||||
url.searchParams.set("rootUri", fileUrl(root));
|
||||
url.searchParams.set("treeView", "filetree");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function writeWorkspaceManifest(root, ownerId, taskId) {
|
||||
const metadataDir = path.join(root, ".mnote");
|
||||
fs.mkdirSync(metadataDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(metadataDir, "workspace.json"),
|
||||
`${JSON.stringify({
|
||||
workspaceId: `local-ws:${ownerId}:${taskId}`,
|
||||
ownerId,
|
||||
createdAt: new Date().toISOString(),
|
||||
capabilities: ["local_files", "markdown_edit", "asset_upload"],
|
||||
}, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
async function quickLogin(page) {
|
||||
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
const quickLoginButton = page.getByRole("button", { name: "测试账号快速登录" });
|
||||
if (await quickLoginButton.count()) {
|
||||
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForURL((url) => url.pathname === "/", { timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function activatePrimaryPageTab(page) {
|
||||
const pageTab = page.locator('[data-mnote-main-tab="page"][data-pane-role="primary"]').first();
|
||||
if (await pageTab.count()) {
|
||||
await pageTab.click({ timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||||
}
|
||||
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadAttachmentViaPrimarySlash(page, fileName, markdownContent) {
|
||||
await activatePrimaryPageTab(page);
|
||||
const editor = page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first();
|
||||
await editor.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.keyboard.press("End").catch(() => undefined);
|
||||
await page.keyboard.type("/");
|
||||
const item = page
|
||||
.locator('.document-pane[data-pane-role="primary"] [data-testid="slash-item-upload-attachment"]')
|
||||
.first();
|
||||
await item.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent("filechooser", { timeout: UI_TIMEOUT_MS }),
|
||||
item.click({ timeout: UI_TIMEOUT_MS }),
|
||||
]);
|
||||
await fileChooser.setFiles({
|
||||
name: fileName,
|
||||
mimeType: "text/markdown",
|
||||
buffer: Buffer.from(markdownContent, "utf8"),
|
||||
});
|
||||
await page.waitForFunction(
|
||||
(name) => {
|
||||
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
|
||||
return editor instanceof HTMLElement && (editor.textContent || "").includes(name);
|
||||
},
|
||||
fileName,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForFileExists(filePath, timeoutMs) {
|
||||
const startedAt = Date.now();
|
||||
while (!fs.existsSync(filePath) && Date.now() - startedAt < timeoutMs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
|
||||
// ─── main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task479-lifecycle-"));
|
||||
const relativePath = "README.md";
|
||||
const resourceDirName = "README";
|
||||
const resourceDir = path.join(root, resourceDirName);
|
||||
fs.mkdirSync(resourceDir, { recursive: true });
|
||||
writeWorkspaceManifest(root, "user_real", "task479");
|
||||
|
||||
// 主正文:README.md,引用一个已存在的附件 resource-note.md
|
||||
fs.writeFileSync(
|
||||
path.join(root, relativePath),
|
||||
[
|
||||
"---",
|
||||
"title: Lifecycle Smoke",
|
||||
"---",
|
||||
"",
|
||||
"# Lifecycle Smoke",
|
||||
"",
|
||||
"正文内容不变。",
|
||||
"",
|
||||
"[resource-note.md](" + resourceDirName + "/resource-note.md)",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(root, resourceDirName, "resource-note.md"), "# Resource Note\n\n已有资源正文\n", "utf8");
|
||||
|
||||
// 为 Check 6 准备一个带 H1 的文件(文件名不含空格,避免空格编码干扰测试)
|
||||
fs.writeFileSync(path.join(root, "MyNotes.md"), "# Body Heading\n\n正文内容\n", "utf8");
|
||||
|
||||
// 为 Check 5 准备足够的文件树条目来产生可滚动区域
|
||||
fs.mkdirSync(path.join(root, "docs"), { recursive: true });
|
||||
for (let i = 0; i < 30; i++) {
|
||||
fs.writeFileSync(path.join(root, "docs", `doc-${String(i).padStart(2, "0")}.md`), `# Doc ${i}\n`, "utf8");
|
||||
}
|
||||
// 为 Check 5 准备的附件 PDF
|
||||
fs.writeFileSync(path.join(resourceDir, "report-a.pdf"), Buffer.from("%PDF-1.4\n% task479 report-a\n", "utf8"));
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: process.env.HEADFUL !== "1",
|
||||
executablePath: CHROMIUM_EXECUTABLE_PATH,
|
||||
});
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1360, height: 900 },
|
||||
extraHTTPHeaders: {
|
||||
"x-mnote-actor-id": "user_real",
|
||||
"x-mnote-actor-type": "user",
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const popups = [];
|
||||
page.on("popup", (popup) => popups.push(popup));
|
||||
|
||||
const screenshots = [];
|
||||
const checks = {};
|
||||
let overallOk = true;
|
||||
|
||||
try {
|
||||
// ── Login ──
|
||||
await quickLogin(page);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Check 1: 主编辑区上传 / drop 附件后,附件应进入当前 Markdown 页面的资源目录
|
||||
// (README/uploaded-one.md 不是 root 同级)
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const checkId = "editor-upload-to-page-resource-dir";
|
||||
try {
|
||||
await page.goto(documentUrl(root, relativePath), {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
// 通过主编辑区斜杠上传上传一个 MD 文件
|
||||
await uploadAttachmentViaPrimarySlash(page, "uploaded-one.md", "# Uploaded One\n\n第一个上传附件\n");
|
||||
|
||||
// 等待后端 write_local_markdown_asset() 落盘
|
||||
const correctPath = path.join(resourceDir, "uploaded-one.md");
|
||||
const wrongPath = path.join(root, "uploaded-one.md");
|
||||
const landedInResourceDir = await waitForFileExists(correctPath, UI_TIMEOUT_MS);
|
||||
const landedInRoot = fs.existsSync(wrongPath);
|
||||
|
||||
assert(
|
||||
landedInResourceDir,
|
||||
`上传文件应落在 page resource directory (${correctPath})。`
|
||||
+ ` landedInRoot=${landedInRoot} correctPath=${correctPath}`,
|
||||
);
|
||||
|
||||
checks[checkId] = {
|
||||
ok: true,
|
||||
message: `上传文件落在资源目录 ${correctPath}`,
|
||||
landedInResourceDir,
|
||||
landedInRoot,
|
||||
};
|
||||
} catch (err) {
|
||||
overallOk = false;
|
||||
checks[checkId] = {
|
||||
ok: false,
|
||||
message: `主编辑区上传落盘目录检查失败: ${err.message}`,
|
||||
error: err.stack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Check 2: 文件树 folder row 外部 drop 到目标文件夹时,文件应进入该目标 folder
|
||||
// 而不是当前 Markdown 页面的资源目录
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const checkId = "filetree-folder-drop-to-target";
|
||||
try {
|
||||
await page.goto(documentUrl(root, relativePath), {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
// 等待文件树就绪
|
||||
const docsFolderRow = page.locator(
|
||||
'#sidebar-file-tree-root .tree-row[data-row-id="local:folder:docs"]',
|
||||
).first();
|
||||
await docsFolderRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
|
||||
const droppedFileName = "drag-dropped-note.md";
|
||||
const droppedContent = "# Drag Dropped\n\n从外部拖入\n";
|
||||
|
||||
// 使用 dispatchEvent 模拟外部文件拖入 docs 文件夹
|
||||
await docsFolderRow.dispatchEvent("dragover", {
|
||||
dataTransfer: await page.evaluateHandle(
|
||||
({ fileName, fileContent }) => {
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(new File([fileContent], fileName, { type: "text/markdown" }));
|
||||
return dt;
|
||||
},
|
||||
{ fileName: droppedFileName, fileContent: droppedContent },
|
||||
),
|
||||
});
|
||||
await docsFolderRow.dispatchEvent("drop", {
|
||||
dataTransfer: await page.evaluateHandle(
|
||||
({ fileName, fileContent }) => {
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(new File([fileContent], fileName, { type: "text/markdown" }));
|
||||
return dt;
|
||||
},
|
||||
{ fileName: droppedFileName, fileContent: droppedContent },
|
||||
),
|
||||
});
|
||||
|
||||
// 等待文件落盘
|
||||
const droppedPath = path.join(root, "docs", droppedFileName);
|
||||
const exists = await waitForFileExists(droppedPath, UI_TIMEOUT_MS);
|
||||
assert(exists, `拖入 docs 文件夹后文件应出现在 ${droppedPath}`);
|
||||
|
||||
// 确保没有出现在 root 或 README/ 下
|
||||
const wrongPath = path.join(root, droppedFileName);
|
||||
const wrongResourcePath = path.join(resourceDir, droppedFileName);
|
||||
assert(
|
||||
!fs.existsSync(wrongPath) && !fs.existsSync(wrongResourcePath),
|
||||
`拖入 docs 文件夹的文件不应落在 root (${wrongPath}) 或 resourceDir (${wrongResourcePath}) 下`,
|
||||
);
|
||||
|
||||
checks[checkId] = {
|
||||
ok: true,
|
||||
message: `外部拖入文件进入目标文件夹 docs/`,
|
||||
droppedPath,
|
||||
exists,
|
||||
};
|
||||
} catch (err) {
|
||||
overallOk = false;
|
||||
checks[checkId] = {
|
||||
ok: false,
|
||||
message: `文件树 folder drop 检查失败: ${err.message}`,
|
||||
error: err.stack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Check 3: 删除真实附件文件后,刷新页面,正文中的 Markdown 链接仍应可见
|
||||
// 如果当前实现失败,先记录当前 failure,但断言应表达预期行为
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const checkId = "broken-link-after-real-file-delete";
|
||||
try {
|
||||
await page.goto(documentUrl(root, relativePath), {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
// 先确认正文中有附件链接
|
||||
const linkTextBefore = await page.evaluate(() => {
|
||||
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
|
||||
return editor instanceof HTMLElement ? editor.textContent || "" : "";
|
||||
});
|
||||
assert(
|
||||
linkTextBefore.includes("resource-note.md"),
|
||||
`初始正文应包含附件链接 resource-note.md,实际内容: ${linkTextBefore.slice(0, 500)}`,
|
||||
);
|
||||
|
||||
// 从磁盘删除真实附件文件
|
||||
const assetPath = path.join(resourceDir, "resource-note.md");
|
||||
assert(fs.existsSync(assetPath), "测试前置条件:附件文件应存在");
|
||||
fs.unlinkSync(assetPath);
|
||||
|
||||
// 刷新页面
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
// 给异步解析和渲染留时间
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 检查正文是否仍包含附件链接文本
|
||||
const linkTextAfter = await page.evaluate(() => {
|
||||
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
|
||||
return editor instanceof HTMLElement ? editor.textContent || "" : "";
|
||||
});
|
||||
|
||||
// 预期行为:链接应保留
|
||||
const linkPreserved = linkTextAfter.includes("resource-note.md");
|
||||
|
||||
checks[checkId] = {
|
||||
ok: linkPreserved,
|
||||
message: linkPreserved
|
||||
? "删除真实附件后刷新,正文链接仍保留"
|
||||
: "删除真实附件后刷新,正文链接消失(当前实现未保留 broken link)",
|
||||
assetPath,
|
||||
deleted: true,
|
||||
linkPreserved,
|
||||
editorText: linkTextAfter.slice(0, 500),
|
||||
};
|
||||
|
||||
if (!linkPreserved) {
|
||||
overallOk = false;
|
||||
}
|
||||
} catch (err) {
|
||||
overallOk = false;
|
||||
checks[checkId] = {
|
||||
ok: false,
|
||||
message: `删除真实附件后正文链接保留检查失败: ${err.message}`,
|
||||
error: err.stack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Check 4: 刚进入 local folder、未先打开任何 Markdown 文件时,
|
||||
// 点击 PDF / Markdown 附件 row 应在主编辑区 tab 打开,不应产生 popup。
|
||||
// 若当前 UI 无法直接进入这种状态,记录原因并把该 check 标记为 skipped
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const checkId = "direct-attach-open-without-active-md";
|
||||
try {
|
||||
// 使用独立 asset-only 工作区,确保 local folder 首屏没有 active Markdown document。
|
||||
const assetOnlyRoot = path.join(root, "asset-only-workspace");
|
||||
fs.mkdirSync(path.join(assetOnlyRoot, "attachments"), { recursive: true });
|
||||
writeWorkspaceManifest(assetOnlyRoot, "user_real", "task479-asset-only");
|
||||
fs.writeFileSync(path.join(assetOnlyRoot, "attachments", "report-a.pdf"), Buffer.from("%PDF-1.4\n% task479 asset-only report-a\n", "utf8"));
|
||||
|
||||
const homeUrl = localFolderHomeUrl(assetOnlyRoot);
|
||||
await page.goto(homeUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
|
||||
// 等待文件树就绪
|
||||
const fileTreeRoot = page.locator("#sidebar-file-tree-root").first();
|
||||
await fileTreeRoot.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
|
||||
const hasResourceHost = await page.locator('[data-mnote-resource-tab-host][data-pane-role="primary"]')
|
||||
.first()
|
||||
.count()
|
||||
.then((count) => count > 0)
|
||||
.catch(() => false);
|
||||
|
||||
const resourceFolderRow = page
|
||||
.locator('#sidebar-file-tree-root .tree-row[data-node-id="local:node:attachments"], #sidebar-file-tree-root .tree-row[data-row-id="local:folder:attachments"]')
|
||||
.first();
|
||||
if (await resourceFolderRow.isVisible().catch(() => false)) {
|
||||
const expanded = await resourceFolderRow.getAttribute("aria-expanded").catch(() => null);
|
||||
const resourceFolderToggle = resourceFolderRow.locator('[data-testid="filetree-toggle"]').first();
|
||||
if (expanded !== "true" && await resourceFolderToggle.isVisible().catch(() => false)) {
|
||||
await resourceFolderToggle.click({ timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
// 找到附件 row (data-testid="filetree-asset-row" 或 data-row-kind="asset"/"attachment")
|
||||
const pdfAssetRow = page.locator('[data-testid="filetree-asset-row"]', {
|
||||
hasText: "report-a.pdf",
|
||||
}).first();
|
||||
await pdfAssetRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const hasPdfRow = await pdfAssetRow.isVisible().catch(() => false);
|
||||
|
||||
assert(hasResourceHost, "无 active Markdown document 时应渲染主编辑区 resource tab host");
|
||||
assert(hasPdfRow, "无 active Markdown document 时应渲染 report-a.pdf 附件 row");
|
||||
|
||||
const popupCountBefore = popups.length;
|
||||
await pdfAssetRow.locator(".tree-link").click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const popupCountAfter = popups.length;
|
||||
const resourceTabVisible = await page.locator('[data-testid="mnote-resource-tab-host"]:not([hidden])')
|
||||
.isVisible().catch(() => false);
|
||||
const resourceTabTitle = await page
|
||||
.locator('[data-mnote-main-tab][data-pane-role="primary"] .mnote-main-tab-title', { hasText: "report-a.pdf" })
|
||||
.first()
|
||||
.textContent({ timeout: UI_TIMEOUT_MS })
|
||||
.catch(() => "");
|
||||
|
||||
assert(
|
||||
popupCountAfter === popupCountBefore,
|
||||
`点击附件 row 不应产生新浏览器窗口/标签页,popup 数:${popupCountBefore} → ${popupCountAfter}`,
|
||||
);
|
||||
assert(resourceTabVisible, "点击附件 row 后应显示主编辑区 resource tab host");
|
||||
assert((resourceTabTitle || "").includes("report-a.pdf"), `资源 tab 标题应包含 report-a.pdf,实际: ${resourceTabTitle}`);
|
||||
|
||||
checks[checkId] = {
|
||||
ok: true,
|
||||
message: "无 active Markdown document 时,点击附件 row 后主编辑区 resource tab 打开且不产生 popup",
|
||||
popupCount: popupCountAfter - popupCountBefore,
|
||||
resourceTabVisible,
|
||||
resourceTabTitle,
|
||||
};
|
||||
} catch (err) {
|
||||
overallOk = false;
|
||||
checks[checkId] = {
|
||||
ok: false,
|
||||
message: `直接打开附件检查异常: ${err.message}`,
|
||||
error: err.stack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Check 5: 文件树滚动到底和 resize handle 上滚轮后,
|
||||
// document.scrollingElement.scrollTop 应保持 0 或不产生底部空白
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const checkId = "filetree-scroll-no-body-scroll";
|
||||
try {
|
||||
await page.goto(documentUrl(root, relativePath), {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.locator("#sidebar-file-tree-root").waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
// 先让文件树出现
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 获取滚动前的 body scrollTop
|
||||
const scrollBefore = await page.evaluate(() => document.scrollingElement?.scrollTop ?? 0);
|
||||
|
||||
// 尝试让文件树滚动到底:在文件树上用鼠标滚轮
|
||||
const fileTree = page.locator("#sidebar-file-tree-root").first();
|
||||
await fileTree.hover({ timeout: UI_TIMEOUT_MS });
|
||||
|
||||
// 多次在文件树上滚轮模拟快速滚到底
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await page.mouse.wheel(0, 120);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
}
|
||||
|
||||
// 再在文件树区域一次性大量滚轮
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await page.mouse.wheel(0, 300);
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
}
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const scrollAfter = await page.evaluate(() => document.scrollingElement?.scrollTop ?? 0);
|
||||
|
||||
// 预期:body scrollTop 不应被文件树滚轮事件改变
|
||||
const bodyNotScrolled = scrollAfter <= scrollBefore;
|
||||
|
||||
checks[checkId] = {
|
||||
ok: bodyNotScrolled,
|
||||
message: bodyNotScrolled
|
||||
? "文件树内滚轮没有导致 body 滚动"
|
||||
: "文件树滚轮导致 body 产生了滚动(出现底部空白)",
|
||||
scrollBefore,
|
||||
scrollAfter,
|
||||
delta: scrollAfter - scrollBefore,
|
||||
};
|
||||
|
||||
if (!bodyNotScrolled) {
|
||||
overallOk = false;
|
||||
}
|
||||
} catch (err) {
|
||||
overallOk = false;
|
||||
checks[checkId] = {
|
||||
ok: false,
|
||||
message: `文件树滚动 body 空白检查失败: ${err.message}`,
|
||||
error: err.stack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Check 6: 本地 Markdown 标题:File Name.md 且正文第一行 # Body Heading 时,
|
||||
// File Tree 显示 "File Name.md",正文仍显示 "Body Heading"
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const checkId = "markdown-title-from-filename-not-h1";
|
||||
try {
|
||||
const fileName = "MyNotes.md";
|
||||
const fileRelPath = fileName;
|
||||
// 确认文件已被写入
|
||||
assert(fs.existsSync(path.join(root, fileName)), "File Name.md 应存在");
|
||||
|
||||
// 导航到该文件
|
||||
await page.goto(documentUrl(root, fileRelPath), {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
// 等待文件树就绪
|
||||
await page.locator("#sidebar-file-tree-root").waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 从文件树获知 MyNotes.md 的标题
|
||||
const treeTitle = await page.evaluate(() => {
|
||||
const rows = document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-row-id^="local:markdown:"]');
|
||||
for (const row of rows) {
|
||||
const text = row.querySelector(".tree-link-title")?.textContent?.trim() || row.textContent?.trim() || "";
|
||||
if (text.includes("MyNotes")) return text;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// 获取正文第一行 / 标题
|
||||
const bodyHeading = await page.evaluate(() => {
|
||||
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
|
||||
if (!(editor instanceof HTMLElement)) return null;
|
||||
// 尝试获取第一个 h1
|
||||
const h1 = editor.querySelector("h1");
|
||||
if (h1 instanceof HTMLElement) return h1.textContent?.trim() || "";
|
||||
// fallback: textContent 第一行
|
||||
return (editor.textContent || "").split("\n").map((l) => l.trim()).filter(Boolean)[0] || "";
|
||||
});
|
||||
|
||||
// 预期:文件树显示 MyNotes.md(文件名),正文显示 Body Heading(H1 内容)
|
||||
const treeShowsFileName = treeTitle && treeTitle.includes("MyNotes.md");
|
||||
const bodyShowsHeading = bodyHeading && bodyHeading.includes("Body Heading");
|
||||
|
||||
checks[checkId] = {
|
||||
ok: !!(treeShowsFileName && bodyShowsHeading),
|
||||
message: `treeTitle="${treeTitle}" bodyHeading="${bodyHeading}"`,
|
||||
treeTitle,
|
||||
bodyHeading,
|
||||
treeShowsFileName,
|
||||
bodyShowsHeading,
|
||||
};
|
||||
} catch (err) {
|
||||
overallOk = false;
|
||||
checks[checkId] = {
|
||||
ok: false,
|
||||
message: `标题来源检查失败: ${err.message}`,
|
||||
error: err.stack,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── 截图 ──
|
||||
{
|
||||
const shotTimestamp = Date.now();
|
||||
const shotDir = path.join(root, `screenshots-${shotTimestamp}`);
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(shotDir, "final-state.png"), fullPage: false });
|
||||
screenshots.push(path.join(shotDir, "final-state.png"));
|
||||
}
|
||||
|
||||
// ── 结果 ──
|
||||
const result = {
|
||||
ok: overallOk,
|
||||
root,
|
||||
checks,
|
||||
screenshots,
|
||||
popups: popups.length,
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} finally {
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
// 清理临时目录,保留 result JSON 被 console.log 输出即可
|
||||
// 如果用户需要查看留下的文件,可设置 KEEP_TEMP=1
|
||||
if (!process.env.KEEP_TEMP) {
|
||||
try { fs.rmSync(root, { recursive: true, force: true }); } catch (_) { /* ignore cleanup errors */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error && error.stack ? error.stack : error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user