"use strict"; // 说明: // - 这是 task-019 的最小真实浏览器回归脚本。 // - 目标只覆盖文档页元信息、Sidebar、BlockNote 保存链,不扩大到 Mindmap / OnlyOffice。 // - 脚本会先创建一篇临时页面,完成回归后再彻底删除,避免污染现有数据。 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; 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); });