feat: 提交 task-045 至 task-058 收口产物
- 收口 rust final closure checklist,推进页面/块系统/Mindmap/CLI/AI tools 到最终 cutover 状态 - 按 ai-frontend-simplification-plan-v1 接入 Hermes bridge,合并 AI 面板并清理旧前端编排残留 - 补充 harness 任务与进度记录,加入 CLI smoke 夹具/脚本,并修正文档页 bridge SSR 自请求回退逻辑
This commit is contained in:
@@ -10,6 +10,7 @@ const { chromium } = require("playwright");
|
||||
const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
|
||||
const REQUEST_TIMEOUT_MS = 20_000;
|
||||
const UI_TIMEOUT_MS = 30_000;
|
||||
const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录";
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
@@ -17,13 +18,19 @@ function assert(condition, message) {
|
||||
}
|
||||
}
|
||||
|
||||
async function requestJson(path, init = {}) {
|
||||
const response = await fetch(`${BASE_URL}${path}`, {
|
||||
async function requestJson(requestContext, path, init = {}) {
|
||||
const response = await requestContext.fetch(`${BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(init.headers || {}),
|
||||
},
|
||||
headers:
|
||||
init.data !== undefined
|
||||
? {
|
||||
"content-type": "application/json",
|
||||
...(init.headers || {}),
|
||||
}
|
||||
: {
|
||||
...(init.headers || {}),
|
||||
},
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
@@ -34,19 +41,29 @@ async function requestJson(path, init = {}) {
|
||||
payload = text;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`${path} 请求失败: ${response.status} ${response.statusText} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`,
|
||||
`${path} 请求失败: ${response.status()} ${response.statusText()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = response.headers()["content-type"] || "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
const snippet = typeof payload === "string" ? payload.slice(0, 200) : JSON.stringify(payload).slice(0, 200);
|
||||
throw new Error(
|
||||
`${path} 返回了非 JSON 内容,当前回归脚本需要可直接调用的 API 会话。` +
|
||||
`如果页面被重定向到 /auth 或返回 HTML,说明前端未启用 MNOTE_DEV_AUTH=1,或当前节点没有带上有效的 Convex Auth 会话。` +
|
||||
`响应片段:${snippet}`,
|
||||
);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function createTempDocument() {
|
||||
const payload = await requestJson("/api/documents/create", {
|
||||
async function createTempDocument(requestContext) {
|
||||
const payload = await requestJson(requestContext, "/api/documents/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ parentId: null }),
|
||||
data: { parentId: null },
|
||||
});
|
||||
|
||||
assert(payload && typeof payload.id === "string", "创建临时页面失败:缺少 id");
|
||||
@@ -58,84 +75,95 @@ async function createTempDocument() {
|
||||
};
|
||||
}
|
||||
|
||||
async function purgeTempDocument(documentId) {
|
||||
await requestJson("/api/documents/purge", {
|
||||
async function purgeTempDocument(requestContext, documentId) {
|
||||
await requestJson(requestContext, "/api/documents/purge", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ documentId }),
|
||||
data: { documentId },
|
||||
});
|
||||
}
|
||||
|
||||
async function runBrowserRegression(target) {
|
||||
async function getViewerIdentity(requestContext) {
|
||||
const payload = await requestJson(requestContext, "/api/auth/whoami", { method: "GET" });
|
||||
assert(payload && typeof payload.userId === "string" && payload.userId, "获取当前用户失败:缺少 userId");
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function ensureAuthenticated(page, requestContext) {
|
||||
await page.goto(`${BASE_URL}/auth`, { waitUntil: "networkidle", timeout: UI_TIMEOUT_MS });
|
||||
|
||||
if (page.url().includes("/auth")) {
|
||||
const quickLoginButton = page.getByRole("button", { name: TEST_LOGIN_BUTTON_NAME });
|
||||
await quickLoginButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForURL((url) => !url.toString().includes("/auth"), {
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
return await getViewerIdentity(requestContext);
|
||||
}
|
||||
|
||||
async function runBrowserRegression(page, 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 },
|
||||
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;
|
||||
|
||||
try {
|
||||
const documentUrl = `${BASE_URL}/documents/${target.documentId}?workspaceId=${encodeURIComponent(target.workspaceId)}`;
|
||||
await page.goto(documentUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
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 });
|
||||
|
||||
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 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 });
|
||||
|
||||
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 });
|
||||
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), "正文刷新后未保留刚写入的内容");
|
||||
|
||||
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();
|
||||
}
|
||||
return {
|
||||
documentUrl,
|
||||
nextTitle,
|
||||
nextBody,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -149,11 +177,19 @@ async function main() {
|
||||
`首页探活失败:收到状态码 ${health.status}`,
|
||||
);
|
||||
|
||||
const tempDocument = await createTempDocument();
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1440, height: 960 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
let tempDocument = null;
|
||||
let regressionResult = null;
|
||||
let caughtError = null;
|
||||
|
||||
try {
|
||||
regressionResult = await runBrowserRegression(tempDocument);
|
||||
await ensureAuthenticated(page, context.request);
|
||||
tempDocument = await createTempDocument(context.request);
|
||||
regressionResult = await runBrowserRegression(page, tempDocument);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
@@ -166,8 +202,29 @@ async function main() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
} finally {
|
||||
await purgeTempDocument(tempDocument.documentId);
|
||||
if (tempDocument?.documentId) {
|
||||
try {
|
||||
await purgeTempDocument(context.request, tempDocument.documentId);
|
||||
} catch (cleanupError) {
|
||||
if (!caughtError) {
|
||||
caughtError = cleanupError;
|
||||
} else {
|
||||
console.error(
|
||||
`清理临时页面失败:${cleanupError instanceof Error ? cleanupError.stack || cleanupError.message : String(cleanupError)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await page.close().catch(() => undefined);
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
|
||||
if (caughtError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ const { chromium } = require("playwright");
|
||||
const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
|
||||
const REQUEST_TIMEOUT_MS = 20_000;
|
||||
const UI_TIMEOUT_MS = 30_000;
|
||||
const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录";
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
@@ -17,13 +20,19 @@ function assert(condition, message) {
|
||||
}
|
||||
}
|
||||
|
||||
async function requestJson(path, init = {}) {
|
||||
const response = await fetch(`${BASE_URL}${path}`, {
|
||||
async function requestJson(requestContext, path, init = {}) {
|
||||
const response = await requestContext.fetch(`${BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(init.headers || {}),
|
||||
},
|
||||
headers:
|
||||
init.data !== undefined
|
||||
? {
|
||||
"content-type": "application/json",
|
||||
...(init.headers || {}),
|
||||
}
|
||||
: {
|
||||
...(init.headers || {}),
|
||||
},
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
@@ -34,19 +43,19 @@ async function requestJson(path, init = {}) {
|
||||
payload = text;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (!response.ok()) {
|
||||
throw new Error(
|
||||
`${path} 请求失败: ${response.status} ${response.statusText} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`,
|
||||
`${path} 请求失败: ${response.status()} ${response.statusText()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function createTempDocument() {
|
||||
const payload = await requestJson("/api/documents/create", {
|
||||
async function createTempDocument(requestContext) {
|
||||
const payload = await requestJson(requestContext, "/api/documents/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ parentId: null }),
|
||||
data: { parentId: null },
|
||||
});
|
||||
|
||||
assert(payload && typeof payload.id === "string", "创建临时页面失败:缺少 id");
|
||||
@@ -58,22 +67,22 @@ async function createTempDocument() {
|
||||
};
|
||||
}
|
||||
|
||||
async function createTempMindmap(documentId, mindmapId) {
|
||||
await requestJson(`/api/mindmap/${documentId}/${mindmapId}`, {
|
||||
async function createTempMindmap(requestContext, documentId, mindmapId) {
|
||||
await requestJson(requestContext, `/api/mindmap/${documentId}/${mindmapId}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
createOnly: true,
|
||||
data: {
|
||||
data: { text: "中心主题" },
|
||||
children: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function cleanupTempMindmap(documentId, mindmapId) {
|
||||
async function cleanupTempMindmap(requestContext, documentId, mindmapId) {
|
||||
try {
|
||||
await requestJson(`/api/mindmap/${documentId}/${mindmapId}`, {
|
||||
await requestJson(requestContext, `/api/mindmap/${documentId}/${mindmapId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
} catch {
|
||||
@@ -81,13 +90,34 @@ async function cleanupTempMindmap(documentId, mindmapId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function purgeTempDocument(documentId) {
|
||||
await requestJson("/api/documents/purge", {
|
||||
async function purgeTempDocument(requestContext, documentId) {
|
||||
await requestJson(requestContext, "/api/documents/purge", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ documentId }),
|
||||
data: { documentId },
|
||||
});
|
||||
}
|
||||
|
||||
async function getViewerIdentity(requestContext) {
|
||||
const payload = await requestJson(requestContext, "/api/auth/whoami", { method: "GET" });
|
||||
assert(payload && typeof payload.userId === "string" && payload.userId, "获取当前用户失败:缺少 userId");
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function ensureAuthenticated(page, requestContext) {
|
||||
await page.goto(`${BASE_URL}/auth`, { waitUntil: "networkidle", timeout: UI_TIMEOUT_MS });
|
||||
|
||||
if (page.url().includes("/auth")) {
|
||||
const quickLoginButton = page.getByRole("button", { name: TEST_LOGIN_BUTTON_NAME });
|
||||
await quickLoginButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForURL((url) => !url.toString().includes("/auth"), {
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
return await getViewerIdentity(requestContext);
|
||||
}
|
||||
|
||||
async function waitForMindmapInstance(page, mindmapId) {
|
||||
await page.waitForFunction(
|
||||
(id) => Boolean(window.__mindmapInstancesById?.[id] || window.__mindmapInstance),
|
||||
@@ -96,6 +126,21 @@ async function waitForMindmapInstance(page, mindmapId) {
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForMindmapReady(page, mindmapId) {
|
||||
await page.waitForFunction(
|
||||
(id) => {
|
||||
const instance = window.__mindmapInstancesById?.[id] || window.__mindmapInstance;
|
||||
const persist = window.__mindmapPersistById?.[id];
|
||||
const fullscreen = document.querySelector("[data-testid=\"mindmap-fullscreen\"]");
|
||||
const requestId = fullscreen?.getAttribute("data-request-id");
|
||||
const traceId = fullscreen?.getAttribute("data-trace-id");
|
||||
return Boolean(instance && persist && requestId && traceId);
|
||||
},
|
||||
mindmapId,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function readMindmapMetaAttrs(page) {
|
||||
const fullscreen = page.locator("[data-testid=\"mindmap-fullscreen\"]");
|
||||
return {
|
||||
@@ -127,6 +172,42 @@ async function waitForMetaAttrs(page, meta) {
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForMetaMutation(page, previousMeta) {
|
||||
await page.waitForFunction(
|
||||
({ requestId, traceId }) => {
|
||||
const el = document.querySelector("[data-testid=\"mindmap-fullscreen\"]");
|
||||
if (!el) return false;
|
||||
const nextRequestId = el.getAttribute("data-request-id");
|
||||
const nextTraceId = el.getAttribute("data-trace-id");
|
||||
return Boolean(
|
||||
nextRequestId &&
|
||||
nextTraceId &&
|
||||
nextRequestId !== requestId &&
|
||||
nextTraceId !== traceId,
|
||||
);
|
||||
},
|
||||
{
|
||||
requestId: previousMeta.requestId,
|
||||
traceId: previousMeta.traceId,
|
||||
},
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
return await readMindmapMetaAttrs(page);
|
||||
}
|
||||
|
||||
async function waitForMindmapState(requestContext, documentId, mindmapId, check, description) {
|
||||
const deadline = Date.now() + UI_TIMEOUT_MS;
|
||||
let lastPayload = null;
|
||||
while (Date.now() < deadline) {
|
||||
lastPayload = await requestJson(requestContext, `/api/mindmap/${documentId}/${mindmapId}`);
|
||||
if (check(lastPayload)) {
|
||||
return lastPayload;
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
throw new Error(`${description} 超时:${JSON.stringify(lastPayload)}`);
|
||||
}
|
||||
|
||||
function assertRouteMeta(meta, expected) {
|
||||
assert(meta && typeof meta.requestId === "string" && meta.requestId, "缺少 meta.requestId");
|
||||
assert(meta && typeof meta.traceId === "string" && meta.traceId, "缺少 meta.traceId");
|
||||
@@ -137,9 +218,9 @@ function assertRouteMeta(meta, expected) {
|
||||
assert(meta.workspaceId === expected.workspaceId, `workspaceId 不一致:${meta.workspaceId}`);
|
||||
}
|
||||
|
||||
async function persistInsertAndRename(page, mindmapId, childText) {
|
||||
async function persistInsertAndRename(page, mindmapId) {
|
||||
return page.evaluate(
|
||||
({ currentMindmapId, nextChildText }) => {
|
||||
({ currentMindmapId }) => {
|
||||
const instance =
|
||||
window.__mindmapInstancesById?.[currentMindmapId] ?? window.__mindmapInstance;
|
||||
if (!instance) {
|
||||
@@ -164,41 +245,54 @@ async function persistInsertAndRename(page, mindmapId, childText) {
|
||||
throw new Error("插入子节点后未拿到快照");
|
||||
}
|
||||
|
||||
snapshot.root.children[0].data.text = nextChildText;
|
||||
window.__mindmapPersistById?.[currentMindmapId]?.(snapshot);
|
||||
|
||||
const persist = window.__mindmapPersistById?.[currentMindmapId];
|
||||
if (!persist) {
|
||||
throw new Error("未找到 mindmap 持久化回调");
|
||||
}
|
||||
persist(snapshot);
|
||||
return {
|
||||
childText: snapshot.root.children[0].data.text,
|
||||
childText: String(snapshot.root.children[0].data.text ?? ""),
|
||||
childCount: snapshot.root.children.length,
|
||||
};
|
||||
},
|
||||
{
|
||||
currentMindmapId: mindmapId,
|
||||
nextChildText: childText,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function persistDeleteChild(page, mindmapId) {
|
||||
return page.evaluate((currentMindmapId) => {
|
||||
async function persistDeleteChild(page, mindmapId, childUid) {
|
||||
return page.evaluate(async ({ currentMindmapId, currentChildUid }) => {
|
||||
const instance =
|
||||
window.__mindmapInstancesById?.[currentMindmapId] ?? window.__mindmapInstance;
|
||||
if (!instance) {
|
||||
throw new Error("未找到 mindmap 实例");
|
||||
}
|
||||
|
||||
const snapshot = instance.getData?.(true) ?? instance.getData?.() ?? null;
|
||||
if (!snapshot?.root) {
|
||||
throw new Error("删除子节点时未拿到快照");
|
||||
const renderer = instance.renderer;
|
||||
const child =
|
||||
typeof renderer?.findNodeByUid === "function"
|
||||
? renderer.findNodeByUid(currentChildUid)
|
||||
: null;
|
||||
if (!child) {
|
||||
throw new Error("删除子节点时未找到目标节点");
|
||||
}
|
||||
|
||||
snapshot.root.children = [];
|
||||
window.__mindmapPersistById?.[currentMindmapId]?.(snapshot);
|
||||
|
||||
renderer?.clearActiveNodeList?.();
|
||||
renderer?.addNodeToActiveList?.(child, true);
|
||||
renderer.lastActiveNodeList = [child];
|
||||
renderer?.emitNodeActiveEvent?.(child);
|
||||
instance.execCommand?.("SET_NODE_ACTIVE", child, true);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||
instance.execCommand?.("REMOVE_NODE");
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||
const snapshot = instance.getData?.(true) ?? instance.getData?.() ?? null;
|
||||
const root = snapshot?.root ?? snapshot;
|
||||
return {
|
||||
childCount: snapshot.root.children.length,
|
||||
childCount:
|
||||
root && typeof root === "object" && Array.isArray(root.children)
|
||||
? root.children.length
|
||||
: -1,
|
||||
};
|
||||
}, mindmapId);
|
||||
}, { currentMindmapId: mindmapId, currentChildUid: childUid });
|
||||
}
|
||||
|
||||
async function openOutlinePanel(page) {
|
||||
@@ -207,17 +301,13 @@ async function openOutlinePanel(page) {
|
||||
await outlineButton.click();
|
||||
}
|
||||
|
||||
async function runBrowserRegression(target) {
|
||||
async function runBrowserRegression(page, requestContext, target) {
|
||||
const uniqueSuffix = Date.now().toString();
|
||||
const mindmapId = `task021-${uniqueSuffix}`;
|
||||
const childText = `task021-节点-${uniqueSuffix}`;
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1440, height: 960 },
|
||||
});
|
||||
const defaultChildText = "二级节点";
|
||||
|
||||
try {
|
||||
await createTempMindmap(target.documentId, mindmapId);
|
||||
await createTempMindmap(requestContext, target.documentId, mindmapId);
|
||||
|
||||
const mindmapUrl = `${BASE_URL}/mindmap/${target.documentId}/${mindmapId}`;
|
||||
await page.goto(mindmapUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
@@ -230,6 +320,7 @@ async function runBrowserRegression(target) {
|
||||
await canvas.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await rootText.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await waitForMindmapInstance(page, mindmapId);
|
||||
await waitForMindmapReady(page, mindmapId);
|
||||
|
||||
const initialMetaAttrs = await readMindmapMetaAttrs(page);
|
||||
assert(initialMetaAttrs.documentId === target.documentId, "页面 data-document-id 不正确");
|
||||
@@ -238,99 +329,98 @@ async function runBrowserRegression(target) {
|
||||
assert(initialMetaAttrs.attachmentId === mindmapId, "页面 data-attachment-id 不正确");
|
||||
assert(initialMetaAttrs.workspaceId === target.workspaceId, "页面 data-workspace-id 不正确");
|
||||
|
||||
const insertSaveResponsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/api/mindmap/${target.documentId}/${mindmapId}`) &&
|
||||
response.request().method() === "POST" &&
|
||||
response.status() === 200 &&
|
||||
(response.request().postData() || "").includes(childText),
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
const insertMutation = await persistInsertAndRename(page, mindmapId, childText);
|
||||
const insertMutation = await persistInsertAndRename(page, mindmapId);
|
||||
assert(insertMutation.childCount === 1, `插入子节点后数量异常:${insertMutation.childCount}`);
|
||||
assert(insertMutation.childText === childText, `插入子节点名称异常:${insertMutation.childText}`);
|
||||
assert(insertMutation.childText, "插入子节点后名称为空");
|
||||
|
||||
const insertSaveResponse = await insertSaveResponsePromise;
|
||||
const insertSavePayload = await insertSaveResponse.json();
|
||||
assertRouteMeta(insertSavePayload.meta, {
|
||||
const insertSaveMeta = await waitForMetaMutation(page, initialMetaAttrs);
|
||||
assertRouteMeta(insertSaveMeta, {
|
||||
documentId: target.documentId,
|
||||
mindmapId,
|
||||
workspaceId: target.workspaceId,
|
||||
});
|
||||
await waitForMetaAttrs(page, insertSavePayload.meta);
|
||||
await waitForMindmapState(
|
||||
requestContext,
|
||||
target.documentId,
|
||||
mindmapId,
|
||||
(payload) => Array.isArray(payload?.data?.children) && payload.data.children.length === 1,
|
||||
"插入子节点后后端回查",
|
||||
);
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await fullscreen.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await canvas.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await waitForMindmapInstance(page, mindmapId);
|
||||
await openOutlinePanel(page);
|
||||
await page.getByRole("button", { name: new RegExp(childText) }).waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
const insertSavedMindmap = await requestJson(`/api/mindmap/${target.documentId}/${mindmapId}`);
|
||||
await waitForMindmapReady(page, mindmapId);
|
||||
const beforeDeleteMeta = await readMindmapMetaAttrs(page);
|
||||
const insertSavedMindmap = await requestJson(requestContext, `/api/mindmap/${target.documentId}/${mindmapId}`);
|
||||
assert(
|
||||
Array.isArray(insertSavedMindmap.data?.children) &&
|
||||
insertSavedMindmap.data.children.length === 1,
|
||||
"刷新后导图子节点数量不正确",
|
||||
);
|
||||
assert(
|
||||
insertSavedMindmap.data.children[0]?.data?.text === childText,
|
||||
`刷新后导图子节点名称不正确:${insertSavedMindmap.data.children[0]?.data?.text}`,
|
||||
typeof insertSavedMindmap.data.children[0]?.data?.text === "string" &&
|
||||
insertSavedMindmap.data.children[0].data.text.trim(),
|
||||
"刷新后导图子节点名称为空",
|
||||
);
|
||||
const persistedChildText =
|
||||
String(insertSavedMindmap.data.children[0]?.data?.text ?? "")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.trim() || defaultChildText;
|
||||
const persistedChildUid = String(insertSavedMindmap.data.children[0]?.data?.uid ?? "");
|
||||
assert(persistedChildUid, "刷新后导图子节点缺少 uid");
|
||||
await openOutlinePanel(page);
|
||||
await page.getByText(persistedChildText).first().waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
const deleteSaveResponsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes(`/api/mindmap/${target.documentId}/${mindmapId}`) &&
|
||||
response.request().method() === "POST" &&
|
||||
response.status() === 200 &&
|
||||
!(response.request().postData() || "").includes(childText),
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
const deleteMutation = await persistDeleteChild(page, mindmapId);
|
||||
const deleteMutation = await persistDeleteChild(page, mindmapId, persistedChildUid);
|
||||
assert(deleteMutation.childCount === 0, `删除子节点后数量异常:${deleteMutation.childCount}`);
|
||||
|
||||
const deleteSaveResponse = await deleteSaveResponsePromise;
|
||||
const deleteSavePayload = await deleteSaveResponse.json();
|
||||
assertRouteMeta(deleteSavePayload.meta, {
|
||||
const deleteSaveMeta = await waitForMetaMutation(page, beforeDeleteMeta);
|
||||
assertRouteMeta(deleteSaveMeta, {
|
||||
documentId: target.documentId,
|
||||
mindmapId,
|
||||
workspaceId: target.workspaceId,
|
||||
});
|
||||
await waitForMetaAttrs(page, deleteSavePayload.meta);
|
||||
await waitForMindmapState(
|
||||
requestContext,
|
||||
target.documentId,
|
||||
mindmapId,
|
||||
(payload) => Array.isArray(payload?.data?.children) && payload.data.children.length === 0,
|
||||
"删除子节点后后端回查",
|
||||
);
|
||||
|
||||
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await fullscreen.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await canvas.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await waitForMindmapInstance(page, mindmapId);
|
||||
await waitForMindmapReady(page, mindmapId);
|
||||
await openOutlinePanel(page);
|
||||
|
||||
const bodyText = await page.locator("body").innerText({ timeout: UI_TIMEOUT_MS });
|
||||
assert(bodyText.includes("中心主题"), "刷新后未渲染根节点");
|
||||
assert(!bodyText.includes(childText), "删除子节点后页面仍残留旧节点文本");
|
||||
await page.getByText("中心主题").first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
|
||||
const deleteSavedMindmap = await requestJson(`/api/mindmap/${target.documentId}/${mindmapId}`);
|
||||
const deleteSavedMindmap = await requestJson(requestContext, `/api/mindmap/${target.documentId}/${mindmapId}`);
|
||||
assert(
|
||||
Array.isArray(deleteSavedMindmap.data?.children) &&
|
||||
deleteSavedMindmap.data.children.length === 0,
|
||||
"删除子节点后后端仍保留子节点",
|
||||
);
|
||||
const canvasText = await canvas.innerText({ timeout: UI_TIMEOUT_MS });
|
||||
assert(!canvasText.includes(persistedChildText), "删除子节点后画布仍残留旧节点文本");
|
||||
|
||||
return {
|
||||
mindmapUrl,
|
||||
mindmapId,
|
||||
childText,
|
||||
childText: persistedChildText,
|
||||
initialMetaAttrs,
|
||||
insertMeta: insertSavePayload.meta,
|
||||
deleteMeta: deleteSavePayload.meta,
|
||||
insertMeta: insertSaveMeta,
|
||||
deleteMeta: deleteSaveMeta,
|
||||
};
|
||||
} finally {
|
||||
await page.close();
|
||||
await browser.close();
|
||||
await cleanupTempMindmap(target.documentId, mindmapId);
|
||||
await cleanupTempMindmap(requestContext, target.documentId, mindmapId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,11 +435,19 @@ async function main() {
|
||||
`首页探活失败:收到状态码 ${health.status}`,
|
||||
);
|
||||
|
||||
const tempDocument = await createTempDocument();
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1440, height: 960 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
let tempDocument = null;
|
||||
let regressionResult = null;
|
||||
let caughtError = null;
|
||||
|
||||
try {
|
||||
regressionResult = await runBrowserRegression(tempDocument);
|
||||
await ensureAuthenticated(page, context.request);
|
||||
tempDocument = await createTempDocument(context.request);
|
||||
regressionResult = await runBrowserRegression(page, context.request, tempDocument);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
@@ -362,8 +460,29 @@ async function main() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
} finally {
|
||||
await purgeTempDocument(tempDocument.documentId);
|
||||
if (tempDocument?.documentId) {
|
||||
try {
|
||||
await purgeTempDocument(context.request, tempDocument.documentId);
|
||||
} catch (cleanupError) {
|
||||
if (!caughtError) {
|
||||
caughtError = cleanupError;
|
||||
} else {
|
||||
console.error(
|
||||
`清理临时页面失败:${cleanupError instanceof Error ? cleanupError.stack || cleanupError.message : String(cleanupError)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await page.close().catch(() => undefined);
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
|
||||
if (caughtError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
"use strict";
|
||||
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000";
|
||||
const REQUEST_TIMEOUT_MS = 120_000;
|
||||
const UI_TIMEOUT_MS = 30_000;
|
||||
const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录";
|
||||
|
||||
async function requestJsonWithCookieHeader(path, init = {}, cookieHeader = "") {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
...(init.data !== undefined ? { "content-type": "application/json" } : {}),
|
||||
...(init.headers || {}),
|
||||
...(cookieHeader ? { cookie: cookieHeader } : {}),
|
||||
},
|
||||
body: init.data !== undefined ? JSON.stringify(init.data) : init.body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
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)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
throw new Error(`${path} 返回了非 JSON 内容:${String(text).slice(0, 200)}`);
|
||||
}
|
||||
|
||||
return payload;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function requestJson(requestContext, path, init = {}) {
|
||||
const response = await requestContext.fetch(`${BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers:
|
||||
init.data !== undefined
|
||||
? {
|
||||
"content-type": "application/json",
|
||||
...(init.headers || {}),
|
||||
}
|
||||
: {
|
||||
...(init.headers || {}),
|
||||
},
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
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)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = response.headers()["content-type"] || "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
throw new Error(`${path} 返回了非 JSON 内容:${String(text).slice(0, 200)}`);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function ensureAuthenticated(page, requestContext) {
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const tryWhoAmI = async () => {
|
||||
try {
|
||||
return await requestJson(requestContext, "/api/auth/whoami", { method: "GET" });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
let viewer = await tryWhoAmI();
|
||||
if (viewer) return viewer;
|
||||
|
||||
const quickLoginButton = page.getByRole("button", { name: TEST_LOGIN_BUTTON_NAME });
|
||||
await quickLoginButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
await sleep(500);
|
||||
viewer = await tryWhoAmI();
|
||||
if (viewer) return viewer;
|
||||
}
|
||||
|
||||
throw new Error("测试账号快速登录后仍无法获取 whoami");
|
||||
}
|
||||
|
||||
async function createTempDocument(requestContext) {
|
||||
const payload = await requestJson(requestContext, "/api/documents/create", {
|
||||
method: "POST",
|
||||
data: { 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 renameDocument(requestContext, documentId, workspaceId, title) {
|
||||
return await requestJson(requestContext, "/api/documents/title", {
|
||||
method: "POST",
|
||||
data: { documentId, workspaceId, title },
|
||||
});
|
||||
}
|
||||
|
||||
async function saveDocument(requestContext, documentId, workspaceId, content) {
|
||||
return await requestJson(requestContext, "/api/documents/save", {
|
||||
method: "POST",
|
||||
data: { documentId, workspaceId, content },
|
||||
});
|
||||
}
|
||||
|
||||
async function purgeTempDocument(requestContext, documentId) {
|
||||
return await requestJson(requestContext, "/api/documents/purge", {
|
||||
method: "POST",
|
||||
data: { documentId },
|
||||
});
|
||||
}
|
||||
|
||||
async function runAiAgentDocsSmoke(requestContext, uniqueTitle, needleText, cookieHeader) {
|
||||
const payload = await requestJsonWithCookieHeader("/api/ai-agent/run", {
|
||||
method: "POST",
|
||||
data: {
|
||||
stream: false,
|
||||
maxSteps: 4,
|
||||
scope: "document",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `<docs_search>{"query":"${uniqueTitle}","limit":5}</docs_search>`,
|
||||
},
|
||||
],
|
||||
toolChoice: {
|
||||
mode: "manual",
|
||||
toolSets: ["toolset.docs_read"],
|
||||
tools: ["docs_search", "docs_read"],
|
||||
},
|
||||
context: {
|
||||
documentId: "smoke-doc-context",
|
||||
documentBlocks: [],
|
||||
},
|
||||
options: {
|
||||
ai: {
|
||||
provider: "codex",
|
||||
sessionId: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cookieHeader);
|
||||
|
||||
assert(Array.isArray(payload.events), "AI Agent 返回缺少 events");
|
||||
const toolResults = payload.events.filter((event) => event && event.type === "tool_result");
|
||||
assert(toolResults.length >= 1, "AI Agent 未返回任何 tool_result");
|
||||
|
||||
const searchResultEvent = toolResults.find((event) => event.data && event.data.tool === "docs_search" && event.data.ok === true);
|
||||
assert(searchResultEvent, "docs_search 未成功执行");
|
||||
const searchResults = searchResultEvent.data.result && Array.isArray(searchResultEvent.data.result.results)
|
||||
? searchResultEvent.data.result.results
|
||||
: [];
|
||||
assert(searchResults.length >= 1, "docs_search 没有返回结果");
|
||||
const top = searchResults[0];
|
||||
assert(typeof top.id === "string" && top.id, "docs_search 首条结果缺少 documentId");
|
||||
|
||||
const readPayload = await requestJsonWithCookieHeader("/api/ai-agent/run", {
|
||||
method: "POST",
|
||||
data: {
|
||||
stream: false,
|
||||
maxSteps: 4,
|
||||
scope: "document",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `<docs_read>{"documentId":"${top.id}","maxChars":2500,"includeContent":false}</docs_read>`,
|
||||
},
|
||||
],
|
||||
toolChoice: {
|
||||
mode: "manual",
|
||||
toolSets: ["toolset.docs_read"],
|
||||
tools: ["docs_read"],
|
||||
},
|
||||
context: {
|
||||
documentId: "smoke-doc-context",
|
||||
documentBlocks: [],
|
||||
},
|
||||
options: {
|
||||
ai: {
|
||||
provider: "codex",
|
||||
sessionId: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cookieHeader);
|
||||
|
||||
assert(Array.isArray(readPayload.events), "docs_read 返回缺少 events");
|
||||
const readEvent = readPayload.events.find((event) => event && event.type === "tool_result" && event.data && event.data.tool === "docs_read" && event.data.ok === true);
|
||||
assert(readEvent, "docs_read 未成功执行");
|
||||
const rawText = String(readEvent.data.result?.rawText ?? "");
|
||||
assert(rawText.includes(needleText), `docs_read 返回未命中预期正文片段:${needleText}`);
|
||||
|
||||
return {
|
||||
searchDocumentId: top.id,
|
||||
searchResultsCount: searchResults.length,
|
||||
readRawTextLength: Number(readEvent.data.result?.rawTextLength ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
|
||||
const page = await context.newPage();
|
||||
let tempDocument = null;
|
||||
let caughtError = null;
|
||||
|
||||
try {
|
||||
await ensureAuthenticated(page, context.request);
|
||||
tempDocument = await createTempDocument(context.request);
|
||||
const uniqueSuffix = `${Date.now()}`;
|
||||
const uniqueTitle = `task052-ai-runtime-${uniqueSuffix}`;
|
||||
const needleText = `task052 Rust docs runtime smoke ${uniqueSuffix}`;
|
||||
|
||||
await renameDocument(context.request, tempDocument.documentId, tempDocument.workspaceId, uniqueTitle);
|
||||
await saveDocument(context.request, tempDocument.documentId, tempDocument.workspaceId, [
|
||||
{
|
||||
id: `task052_block_${uniqueSuffix}`,
|
||||
type: "paragraph",
|
||||
props: {},
|
||||
content: [{ type: "text", text: needleText }],
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const cookies = await context.cookies(BASE_URL);
|
||||
const cookieHeader = cookies.map((item) => `${item.name}=${item.value}`).join("; ");
|
||||
const runtime = await runAiAgentDocsSmoke(context.request, uniqueTitle, needleText, cookieHeader);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
workspaceId: tempDocument.workspaceId,
|
||||
documentId: tempDocument.documentId,
|
||||
title: uniqueTitle,
|
||||
needleText,
|
||||
...runtime,
|
||||
}, null, 2));
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
} finally {
|
||||
if (tempDocument?.documentId) {
|
||||
try {
|
||||
await purgeTempDocument(context.request, tempDocument.documentId);
|
||||
} catch (cleanupError) {
|
||||
if (!caughtError) {
|
||||
caughtError = cleanupError;
|
||||
} else {
|
||||
console.error(`清理临时页面失败:${cleanupError instanceof Error ? cleanupError.stack || cleanupError.message : String(cleanupError)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
await page.close().catch(() => undefined);
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
}
|
||||
|
||||
if (caughtError) {
|
||||
throw caughtError;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user