Files
mnote/scripts/task803-local-page-reference-restore-smoke.js
T

288 lines
13 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
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 ROOT_PATH = process.env.MNOTE_WOLAI_PAGE_REF_SMOKE_ROOT
|| "/mnt/Data1T/Mnote_data/users/liaibo/workspaces/my-space";
const PARENT_PAGE = process.env.MNOTE_WOLAI_PAGE_REF_PARENT
|| "liaibo的个人空间/项目/项目.md";
const CHILD_PAGE = process.env.MNOTE_WOLAI_PAGE_REF_CHILD
|| "liaibo的个人空间/项目/完结项目/完结项目.md";
const GRANDCHILD_PAGE = process.env.MNOTE_WOLAI_PAGE_REF_GRANDCHILD
|| "liaibo的个人空间/项目/完结项目/爱斯特完结项目/爱斯特完结项目.md";
const USERNAME = process.env.MNOTE_WOLAI_PAGE_REF_USER || "mnote.e2e@example.com";
const PASSWORD = process.env.MNOTE_WOLAI_PAGE_REF_PASSWORD || "MnoteE2E123!";
const OUT_DIR = path.join(process.cwd(), "tmp", "task803-local-page-reference-restore-smoke");
const RESULT_PATH = path.join(OUT_DIR, "result.json");
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| ["/usr/bin/chromium-browser", "/usr/bin/chromium", "/usr/bin/google-chrome-stable", "/snap/bin/chromium"]
.find((candidate) => fs.existsSync(candidate));
function fileUrl(localPath) {
return `file://${localPath}`;
}
function encodeLocalIdSegment(value) {
const bytes = Buffer.from(String(value || ""), "utf8");
let encoded = "";
for (const byte of bytes) {
const character = String.fromCharCode(byte);
if (
(byte >= 48 && byte <= 57)
|| (byte >= 65 && byte <= 90)
|| (byte >= 97 && byte <= 122)
|| character === "."
|| character === "_"
|| character === "-"
) {
encoded += character;
} else {
encoded += `~${byte.toString(16).toUpperCase().padStart(2, "0")}`;
}
}
return encoded;
}
function localMdDocumentId(relativePath) {
return `local-md:${encodeLocalIdSegment(relativePath)}`;
}
function documentUrl(relativePath) {
const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
url.searchParams.set("sourceKind", "local_folder");
url.searchParams.set("rootUri", fileUrl(ROOT_PATH));
url.searchParams.set("treeView", "filetree");
return url.toString();
}
async function signIn(request) {
const response = await request.post(`${BASE_URL}/api/auth`, {
headers: { "content-type": "application/json", accept: "application/json" },
data: {
action: "auth:signIn",
args: {
provider: "password",
params: {
account: USERNAME,
password: PASSWORD,
flow: "signIn",
},
},
},
});
assert.equal(response.status(), 200, `登录失败: ${response.status()} ${await response.text()}`);
}
async function waitForEditor(page) {
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first()
.waitFor({ state: "visible" });
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first()
.waitFor({ state: "visible" });
}
async function pageReferenceTexts(page) {
return page.evaluate(() => Array.from(document.querySelectorAll(".editor-surface .ProseMirror a.mnote-page-block-link"))
.map((anchor) => ({
text: (anchor.textContent || "").trim(),
href: anchor.getAttribute("href") || "",
className: anchor.className || "",
target: anchor.getAttribute("target") || "",
})));
}
async function pageBlockVisual(page, title) {
return page.locator(".editor-surface .ProseMirror a.mnote-page-block-link", { hasText: title }).first().evaluate((anchor) => {
const style = window.getComputedStyle(anchor);
const before = window.getComputedStyle(anchor, "::before");
const row = anchor.closest("p");
const rowStyle = row ? window.getComputedStyle(row) : null;
return {
text: anchor.textContent || "",
display: style.display,
color: style.color,
fontWeight: style.fontWeight,
textDecorationLine: style.textDecorationLine,
beforeWidth: before.width,
beforeHeight: before.height,
rowBackground: rowStyle ? rowStyle.backgroundColor : "",
};
});
}
function assertWolaiPageBlockVisual(visual) {
assert.equal(visual.display, "inline-flex", `页面块应保持图标+标题的 inline-flex 形态: ${JSON.stringify(visual)}`);
assert.notEqual(visual.color, "rgb(0, 0, 238)", `页面块不能退化为浏览器默认蓝色链接: ${JSON.stringify(visual)}`);
assert.ok(visual.textDecorationLine.includes("underline"), `页面块标题应保留 Wolai 风格下划线: ${JSON.stringify(visual)}`);
assert.ok(parseFloat(visual.beforeWidth) >= 14 && parseFloat(visual.beforeHeight) >= 14, `页面块必须有独立页面图标: ${JSON.stringify(visual)}`);
assert.ok(
["", "rgba(0, 0, 0, 0)", "transparent"].includes(visual.rowBackground),
`页面块常态不能渲染整行浅红背景: ${JSON.stringify(visual)}`,
);
}
function installMutationProbe(page) {
return page.evaluate(() => {
if (window.__mnoteTask803MutationObserver) {
window.__mnoteTask803MutationObserver.disconnect();
}
const countElementNodes = (node) => {
if (!(node instanceof Element)) return 0;
return 1 + node.querySelectorAll("*").length;
};
const roots = [
["sidebar", document.querySelector(".mnote-sidebar, #sidebar-file-tree-root, #sidebar-tree-root")],
["workspace", document.querySelector("[data-testid='mnote-document-workspace'], .document-workspace")],
].filter((entry) => entry[1] instanceof Element);
window.__mnoteTask803MutationProbe = {
navigationEntries: performance.getEntriesByType("navigation").length,
records: [],
};
const observer = new MutationObserver((records) => {
for (const record of records) {
const owner = roots.find((entry) => entry[1].contains(record.target));
window.__mnoteTask803MutationProbe.records.push({
owner: owner ? owner[0] : "unknown",
added: Array.from(record.addedNodes).reduce((sum, node) => sum + countElementNodes(node), 0),
removed: Array.from(record.removedNodes).reduce((sum, node) => sum + countElementNodes(node), 0),
});
}
});
roots.forEach((entry) => observer.observe(entry[1], { childList: true, subtree: true }));
window.__mnoteTask803MutationObserver = observer;
});
}
function readMutationProbe(page) {
return page.evaluate(() => {
if (window.__mnoteTask803MutationObserver) {
window.__mnoteTask803MutationObserver.disconnect();
}
const probe = window.__mnoteTask803MutationProbe || { navigationEntries: 0, records: [] };
const summary = {
navigationEntriesBefore: probe.navigationEntries,
navigationEntriesAfter: performance.getEntriesByType("navigation").length,
sidebarAdded: 0,
sidebarRemoved: 0,
workspaceAdded: 0,
workspaceRemoved: 0,
};
for (const record of probe.records || []) {
if (record.owner === "sidebar") {
summary.sidebarAdded += record.added || 0;
summary.sidebarRemoved += record.removed || 0;
}
if (record.owner === "workspace") {
summary.workspaceAdded += record.added || 0;
summary.workspaceRemoved += record.removed || 0;
}
}
return summary;
});
}
function assertLocalPageHref(item, expectedRelativePath) {
const url = new URL(item.href, BASE_URL);
assert.ok(
decodeURIComponent(url.pathname).includes(localMdDocumentId(expectedRelativePath)),
`页面块 href 未指向目标页面: ${JSON.stringify({ item, expectedRelativePath })}`,
);
assert.equal(url.searchParams.get("sourceKind"), "local_folder", JSON.stringify(item));
assert.equal(url.searchParams.get("rootUri"), fileUrl(ROOT_PATH), JSON.stringify(item));
assert.equal(item.target, "_self", JSON.stringify(item));
}
async function main() {
fs.mkdirSync(OUT_DIR, { recursive: true });
const browser = await chromium.launch({
headless: process.env.HEADFUL !== "1",
...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
});
const context = await browser.newContext({
viewport: { width: 1440, height: 960 },
locale: "zh-CN",
});
const page = await context.newPage();
page.setDefaultTimeout(Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 45_000));
const clickRequests = [];
let collectClickRequests = false;
page.on("request", (request) => {
if (!collectClickRequests) return;
clickRequests.push({
method: request.method(),
resourceType: request.resourceType(),
url: request.url(),
});
});
try {
await signIn(context.request);
await page.goto(documentUrl(PARENT_PAGE), { waitUntil: "domcontentloaded" });
await waitForEditor(page);
const parentRefs = await pageReferenceTexts(page);
const completeProjectRef = parentRefs.find((item) => item.text === "完结项目");
assert.ok(completeProjectRef, `父页面没有把 Markdown 子页面链接还原为页面块: ${JSON.stringify(parentRefs)}`);
assertLocalPageHref(completeProjectRef, CHILD_PAGE);
const parentPageBlockVisual = await pageBlockVisual(page, "完结项目");
assertWolaiPageBlockVisual(parentPageBlockVisual);
await page.screenshot({ path: path.join(OUT_DIR, "01-parent-page-reference.png"), fullPage: false });
await installMutationProbe(page);
collectClickRequests = true;
await page.locator(".editor-surface .ProseMirror a.mnote-page-block-link", { hasText: "完结项目" }).first().click();
await page.waitForURL((url) => decodeURIComponent(url.pathname).includes(localMdDocumentId(CHILD_PAGE)));
await waitForEditor(page);
collectClickRequests = false;
const firstClickProbe = await readMutationProbe(page);
const firstClickDocumentRequests = clickRequests.filter((request) => request.resourceType === "document");
const firstClickTreeProjectionRequests = clickRequests.filter((request) => request.url.includes("/api/tree/projections/"));
assert.equal(firstClickDocumentRequests.length, 0, `页面块点击不能触发整页 document 导航: ${JSON.stringify(firstClickDocumentRequests)}`);
assert.equal(firstClickTreeProjectionRequests.length, 0, `页面块点击不应重拉树 projection: ${JSON.stringify(firstClickTreeProjectionRequests)}`);
assert.equal(firstClickProbe.navigationEntriesAfter, firstClickProbe.navigationEntriesBefore, `页面块点击不能新增浏览器 navigation entry: ${JSON.stringify(firstClickProbe)}`);
assert.ok(firstClickProbe.sidebarAdded + firstClickProbe.sidebarRemoved <= 4, `页面块点击不应重建左侧 Sidebar DOM: ${JSON.stringify(firstClickProbe)}`);
const childRefs = await pageReferenceTexts(page);
const childTitles = childRefs.map((item) => item.text);
assert.ok(childTitles.includes("爱斯特完结项目"), `子页面纯文本行没有推断为页面块: ${JSON.stringify(childRefs)}`);
assert.ok(childTitles.includes("药友完结项目"), `子页面缺少其它同级页面块: ${JSON.stringify(childRefs)}`);
assert.ok(childTitles.includes("倍特完结项目"), `子页面缺少其它同级页面块: ${JSON.stringify(childRefs)}`);
assert.ok(!childTitles.includes("22"), `普通文本不应被误还原为页面块: ${JSON.stringify(childRefs)}`);
const grandchildRef = childRefs.find((item) => item.text === "爱斯特完结项目");
assertLocalPageHref(grandchildRef, GRANDCHILD_PAGE);
const childPageBlockVisual = await pageBlockVisual(page, "爱斯特完结项目");
assertWolaiPageBlockVisual(childPageBlockVisual);
await page.screenshot({ path: path.join(OUT_DIR, "02-child-inferred-page-references.png"), fullPage: false });
await page.locator(".editor-surface .ProseMirror a.mnote-page-block-link", { hasText: "爱斯特完结项目" }).first().click();
await page.waitForURL((url) => decodeURIComponent(url.pathname).includes(localMdDocumentId(GRANDCHILD_PAGE)));
await waitForEditor(page);
await page.screenshot({ path: path.join(OUT_DIR, "03-click-opened-grandchild.png"), fullPage: false });
const result = {
ok: true,
parentDocumentId: localMdDocumentId(PARENT_PAGE),
childDocumentId: localMdDocumentId(CHILD_PAGE),
grandchildDocumentId: localMdDocumentId(GRANDCHILD_PAGE),
firstClickProbe,
screenshots: [
path.join(OUT_DIR, "01-parent-page-reference.png"),
path.join(OUT_DIR, "02-child-inferred-page-references.png"),
path.join(OUT_DIR, "03-click-opened-grandchild.png"),
],
};
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
console.log(JSON.stringify(result, null, 2));
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error && error.stack ? error.stack : error);
process.exit(1);
});