2026-04-15 03:06:29 +08:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
// 说明:
|
|
|
|
|
// - 这是 task-019 的最小真实浏览器回归脚本。
|
|
|
|
|
// - 目标只覆盖文档页元信息、Sidebar、BlockNote 保存链,不扩大到 Mindmap / OnlyOffice。
|
|
|
|
|
// - 脚本会先创建一篇临时页面,完成回归后再彻底删除,避免污染现有数据。
|
|
|
|
|
|
|
|
|
|
const { chromium } = require("playwright");
|
|
|
|
|
|
2026-04-15 20:01:12 +08:00
|
|
|
const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
|
2026-04-15 03:06:29 +08:00
|
|
|
const REQUEST_TIMEOUT_MS = 20_000;
|
|
|
|
|
const UI_TIMEOUT_MS = 30_000;
|
|
|
|
|
|
|
|
|
|
function assert(condition, message) {
|
|
|
|
|
if (!condition) {
|
|
|
|
|
throw new Error(message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requestJson(path, init = {}) {
|
|
|
|
|
const response = await fetch(`${BASE_URL}${path}`, {
|
|
|
|
|
...init,
|
|
|
|
|
headers: {
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
...(init.headers || {}),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const text = await response.text();
|
|
|
|
|
let payload = null;
|
|
|
|
|
try {
|
|
|
|
|
payload = text ? JSON.parse(text) : null;
|
|
|
|
|
} catch {
|
|
|
|
|
payload = text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`${path} 请求失败: ${response.status} ${response.statusText} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createTempDocument() {
|
|
|
|
|
const payload = await requestJson("/api/documents/create", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ parentId: null }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert(payload && typeof payload.id === "string", "创建临时页面失败:缺少 id");
|
|
|
|
|
assert(payload && typeof payload.workspace_id === "string", "创建临时页面失败:缺少 workspace_id");
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
documentId: payload.id,
|
|
|
|
|
workspaceId: payload.workspace_id,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function purgeTempDocument(documentId) {
|
|
|
|
|
await requestJson("/api/documents/purge", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ documentId }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runBrowserRegression(target) {
|
|
|
|
|
const uniqueSuffix = Date.now().toString();
|
|
|
|
|
const nextTitle = `task019-ui-${uniqueSuffix}`;
|
|
|
|
|
const nextBody = `task019 正文保存回归 ${uniqueSuffix}`;
|
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
|
|
|
const page = await browser.newPage({
|
|
|
|
|
viewport: { width: 1440, height: 960 },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const documentUrl = `${BASE_URL}/documents/${target.documentId}?workspaceId=${encodeURIComponent(target.workspaceId)}`;
|
|
|
|
|
await page.goto(documentUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
|
|
|
|
const sidebarPanel = page.getByText("页面树");
|
|
|
|
|
const privateSection = page.getByText("私有 / 我的页面");
|
|
|
|
|
const titleInput = page.getByLabel("页面标题");
|
|
|
|
|
const editorSurface = page.locator(".wolai-editor [contenteditable=\"true\"]").first();
|
|
|
|
|
const saveIndicator = page.locator("text=已保存");
|
|
|
|
|
|
|
|
|
|
await sidebarPanel.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await privateSection.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await titleInput.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await editorSurface.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await saveIndicator.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
|
|
|
|
await titleInput.fill(nextTitle);
|
|
|
|
|
const titleSaveResponse = page.waitForResponse(
|
|
|
|
|
(response) =>
|
|
|
|
|
response.url().includes("/api/documents/title") &&
|
|
|
|
|
response.request().method() === "POST" &&
|
|
|
|
|
response.status() === 200,
|
|
|
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
|
|
|
);
|
|
|
|
|
await titleInput.evaluate((node) => {
|
|
|
|
|
node.blur();
|
|
|
|
|
});
|
|
|
|
|
await titleSaveResponse;
|
|
|
|
|
|
|
|
|
|
const saveResponse = page.waitForResponse(
|
|
|
|
|
(response) =>
|
|
|
|
|
response.url().includes("/api/documents/save") &&
|
|
|
|
|
response.request().method() === "POST" &&
|
|
|
|
|
response.status() === 200 &&
|
|
|
|
|
(response.request().postData() || "").includes(nextBody),
|
|
|
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
|
|
|
);
|
|
|
|
|
await editorSurface.click({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await editorSurface.fill(nextBody, { timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await saveResponse;
|
|
|
|
|
await saveIndicator.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
|
|
|
|
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await sidebarPanel.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await privateSection.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await titleInput.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
await page.locator(`text=${nextBody}`).first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
|
|
|
|
const persistedTitle = await titleInput.inputValue();
|
|
|
|
|
assert(persistedTitle === nextTitle, `标题刷新后不一致:期望 ${nextTitle},实际 ${persistedTitle}`);
|
|
|
|
|
const editorText = await page.locator(".wolai-editor").innerText({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
assert(editorText.includes(nextBody), "正文刷新后未保留刚写入的内容");
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
documentUrl,
|
|
|
|
|
nextTitle,
|
|
|
|
|
nextBody,
|
|
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
await page.close();
|
|
|
|
|
await browser.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
const health = await fetch(`${BASE_URL}/`, {
|
|
|
|
|
method: "HEAD",
|
|
|
|
|
redirect: "manual",
|
|
|
|
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
|
|
|
});
|
|
|
|
|
assert(
|
|
|
|
|
[200, 307, 308].includes(health.status),
|
|
|
|
|
`首页探活失败:收到状态码 ${health.status}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const tempDocument = await createTempDocument();
|
|
|
|
|
let regressionResult = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
regressionResult = await runBrowserRegression(tempDocument);
|
|
|
|
|
console.log(
|
|
|
|
|
JSON.stringify(
|
|
|
|
|
{
|
|
|
|
|
ok: true,
|
|
|
|
|
workspaceId: tempDocument.workspaceId,
|
|
|
|
|
documentId: tempDocument.documentId,
|
|
|
|
|
...regressionResult,
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
2,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
await purgeTempDocument(tempDocument.documentId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main().catch((error) => {
|
|
|
|
|
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|