"use strict"; const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const MNOTE_WEB_BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3104").replace(/\/+$/, ""); const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_TIMEOUT_MS || 20_000); const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000); const TEST_LOGIN_BUTTON_NAME = "测试账号快速登录"; const TEST_USERNAME_PREFIX = "测试用户"; 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")) { const snippet = typeof payload === "string" ? payload.slice(0, 200) : JSON.stringify(payload).slice(0, 200); throw new Error(`${path} 返回了非 JSON 内容:${snippet}`); } return payload; } async function createTempDocument(requestContext, parentId = null) { const payload = await requestJson(requestContext, "/api/documents/create", { method: "POST", data: { parentId }, }); 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, workspaceId, documentId, title) { await requestJson(requestContext, "/api/documents/title", { method: "POST", data: { workspaceId, documentId, title, }, }); } async function purgeDocument(requestContext, documentId) { await requestJson(requestContext, "/api/documents/purge", { method: "POST", 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 waitForAuthenticatedRedirect(page, timeout = 8_000) { await page.waitForURL((url) => !url.toString().includes("/auth"), { timeout, waitUntil: "commit", }); } async function isVisible(locator) { try { return await locator.isVisible(); } catch { return false; } } async function completeUsernameSetupIfNeeded(page) { const saveButton = page.getByRole("button", { name: "保存并继续" }); if (!(await isVisible(saveButton))) { return false; } const usernameInput = page.locator('input[name="username"]').last(); await usernameInput.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); const currentValue = ((await usernameInput.inputValue().catch(() => "")) || "").trim(); const username = currentValue || `${TEST_USERNAME_PREFIX}${Date.now().toString().slice(-6)}`; await usernameInput.fill(username, { timeout: UI_TIMEOUT_MS }); await saveButton.click({ timeout: UI_TIMEOUT_MS }); await waitForAuthenticatedRedirect(page, UI_TIMEOUT_MS); return true; } async function registerTestAccountIfNeeded(page) { const switchButton = page.getByRole("button", { name: "还没有账户?立即注册" }); if (!(await isVisible(switchButton))) { return false; } await switchButton.click({ timeout: UI_TIMEOUT_MS }); await page.locator('input[name="email"]').fill("test@example.com", { timeout: UI_TIMEOUT_MS }); await page.locator('input[name="username"]').fill(`${TEST_USERNAME_PREFIX}${Date.now().toString().slice(-6)}`, { timeout: UI_TIMEOUT_MS, }); await page.locator('input[name="password"]').fill("Test123456", { timeout: UI_TIMEOUT_MS }); await page.getByRole("button", { name: "注册" }).click({ timeout: UI_TIMEOUT_MS }); try { await waitForAuthenticatedRedirect(page, UI_TIMEOUT_MS); return true; } catch { return completeUsernameSetupIfNeeded(page); } } async function waitForViewerIdentity(requestContext, attempts = 6) { let lastError = null; for (let index = 0; index < attempts; index += 1) { try { return await getViewerIdentity(requestContext); } catch (error) { lastError = error; await new Promise((resolve) => setTimeout(resolve, 1000)); } } throw lastError instanceof Error ? lastError : new Error("获取当前用户失败"); } async function ensureAuthenticated(page, requestContext) { await page.goto(`${BASE_URL}/auth`, { waitUntil: "commit", timeout: UI_TIMEOUT_MS }); if (page.url().includes("/auth")) { const quickLoginButton = page.getByRole("button", { name: TEST_LOGIN_BUTTON_NAME }); await Promise.race([ page.waitForURL((url) => !url.toString().includes("/auth"), { timeout: UI_TIMEOUT_MS, waitUntil: "commit", }), quickLoginButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }), ]); if (!page.url().includes("/auth")) { return await waitForViewerIdentity(requestContext); } await quickLoginButton.click({ timeout: UI_TIMEOUT_MS }); try { await waitForAuthenticatedRedirect(page, UI_TIMEOUT_MS); } catch { const completedUsername = await completeUsernameSetupIfNeeded(page); if (!completedUsername) { const registered = await registerTestAccountIfNeeded(page); if (!registered) { throw new Error("测试账号登录后仍停留在 /auth,且未进入可恢复的用户名/注册流程"); } } } } return await waitForViewerIdentity(requestContext); } async function prepareTempTreeFixture(requestContext) { const uniqueSuffix = Date.now().toString(); const parentTitle = `task-tree-parent-${uniqueSuffix}`; const childTitle = `task-tree-child-${uniqueSuffix}`; const parent = await createTempDocument(requestContext, null); const child = await createTempDocument(requestContext, parent.documentId); await renameDocument(requestContext, parent.workspaceId, parent.documentId, parentTitle); await renameDocument(requestContext, parent.workspaceId, child.documentId, childTitle); return { uniqueSuffix, workspaceId: parent.workspaceId, parentId: parent.documentId, childId: child.documentId, parentTitle, childTitle, createdIds: [parent.documentId, child.documentId], }; } async function cleanupDocuments(requestContext, createdIds) { for (const documentId of [...createdIds].reverse()) { await purgeDocument(requestContext, documentId); } } async function openDocument(page, workspaceId, documentId) { const url = `${BASE_URL}/documents/${documentId}?workspaceId=${encodeURIComponent(workspaceId)}`; await page.goto(url, { waitUntil: "commit", timeout: UI_TIMEOUT_MS }); return url; } async function openSectionView(page) { const button = page.getByRole("button", { name: "分组" }); await button.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await button.click({ timeout: UI_TIMEOUT_MS }); } async function openFilesystemView(page) { const button = page.getByRole("button", { name: "文件" }); await button.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await button.click({ timeout: UI_TIMEOUT_MS }); } async function ensurePageOptionsVisible(page) { const toggle = page.getByRole("button", { name: /显示页面选项|隐藏页面选项/ }); await toggle.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); const label = (await toggle.getAttribute("aria-label")) || ""; if (label.includes("显示")) { await toggle.click({ timeout: UI_TIMEOUT_MS }); } } module.exports = { BASE_URL, MNOTE_WEB_BASE_URL, REQUEST_TIMEOUT_MS, UI_TIMEOUT_MS, assert, cleanupDocuments, createTempDocument, ensureAuthenticated, ensurePageOptionsVisible, getViewerIdentity, openDocument, openFilesystemView, openSectionView, prepareTempTreeFixture, purgeDocument, renameDocument, requestJson, };