"use strict"; /** * 7-76 P0:浏览器 smoke 标准登录(无「测试账号快速登录」按钮)。 * 优先填表提交 /auth;失败时可回退 API signIn/signUp(由调用方决定)。 */ const DEFAULT_EMAIL = process.env.MNOTE_E2E_EMAIL || "mnote.e2e@example.com"; const DEFAULT_PASSWORD = process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!"; const DEFAULT_USERNAME = process.env.MNOTE_E2E_USERNAME || "mnote-e2e"; /** 方案 A:独立 ops admin(非 AI 主体、非个人笔记主账号) */ const DEFAULT_ADMIN_EMAIL = process.env.MNOTE_ADMIN_EMAIL || "mnote.admin@example.com"; const DEFAULT_ADMIN_PASSWORD = process.env.MNOTE_ADMIN_PASSWORD || "MnoteAdmin123!"; const DEFAULT_ADMIN_USERNAME = process.env.MNOTE_ADMIN_USERNAME || "mnote-admin"; const DEFAULT_UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000); function resolveBaseUrl(baseUrl) { return String(baseUrl || process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace( /\/+$/, "", ); } /** * 在已打开的 /auth 页(或任意页)用标准表单登录。 * @param {import('playwright').Page} page * @param {{ baseUrl?: string, email?: string, password?: string, username?: string, timeoutMs?: number, gotoAuth?: boolean }} [options] */ async function loginViaAuthForm(page, options = {}) { const baseUrl = resolveBaseUrl(options.baseUrl); const email = options.email || DEFAULT_EMAIL; const password = options.password || DEFAULT_PASSWORD; const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS; const gotoAuth = options.gotoAuth !== false; if (gotoAuth || !String(page.url() || "").includes("/auth")) { await page.goto(`${baseUrl}/auth`, { waitUntil: "commit", timeout: timeoutMs }); } const account = page.locator("#account, input[name='account']").first(); const passwordInput = page.locator("#password, input[name='password']").first(); const submit = page.locator("[data-auth-submit], button[type='submit']").first(); await account.waitFor({ state: "visible", timeout: timeoutMs }); await account.fill(email, { timeout: timeoutMs }); await passwordInput.fill(password, { timeout: timeoutMs }); await submit.click({ timeout: timeoutMs }); } /** * API 层 signIn;不存在时可 signUp 再 signIn。 * @param {{ fetch: Function }} requestContext Playwright request 或兼容对象 * @param {{ baseUrl?: string, email?: string, password?: string, username?: string, timeoutMs?: number }} [options] */ async function loginViaAuthApi(requestContext, options = {}) { const baseUrl = resolveBaseUrl(options.baseUrl); const email = options.email || DEFAULT_EMAIL; const password = options.password || DEFAULT_PASSWORD; const username = options.username || DEFAULT_USERNAME; const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS; async function post(params) { const response = await requestContext.fetch(`${baseUrl}/api/auth`, { method: "POST", headers: { "content-type": "application/json" }, data: { action: "auth:signIn", args: { provider: "password", params, }, }, timeout: timeoutMs, }); const text = await response.text(); let payload = null; try { payload = text ? JSON.parse(text) : null; } catch { payload = text; } return { ok: response.ok(), status: response.status(), payload }; } let result = await post({ account: email, email, password, flow: "signIn", }); if (result.ok) { return result.payload; } result = await post({ email, name: username, password, flow: "signUp", }); if (result.ok) { return result.payload; } const details = typeof result.payload === "string" ? result.payload : JSON.stringify(result.payload); throw new Error(`标准 API 登录失败: ${result.status} ${details}`); } /** * 以 ops admin 登录(/admin/*、代签、access-policy、ai-admin settings)。 * AI 主体 mnote-e2e 与个人账号 liaibo 默认无 admin 能力。 */ async function loginAsAdminViaAuthForm(page, options = {}) { return loginViaAuthForm(page, { ...options, email: options.email || DEFAULT_ADMIN_EMAIL, password: options.password || DEFAULT_ADMIN_PASSWORD, username: options.username || DEFAULT_ADMIN_USERNAME, }); } async function loginAsAdminViaAuthApi(requestContext, options = {}) { return loginViaAuthApi(requestContext, { ...options, email: options.email || DEFAULT_ADMIN_EMAIL, password: options.password || DEFAULT_ADMIN_PASSWORD, username: options.username || DEFAULT_ADMIN_USERNAME, }); } /** * 在独立 browser context 里以 admin 会话执行 fn(page),不污染主测用户 cookie。 * 用于:access-policy grants、/api/ai-admin/* 等必须 admin 的 seed。 * @param {import('playwright').Browser} browser * @param {(page: import('playwright').Page) => Promise} fn * @param {{ baseUrl?: string, timeoutMs?: number }} [options] * @returns {Promise} */ async function withAdminBrowserSession(browser, fn, options = {}) { const baseUrl = resolveBaseUrl(options.baseUrl); const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS; const context = await browser.newContext(); const page = await context.newPage(); try { await loginAsAdminViaAuthForm(page, { baseUrl, timeoutMs }); await page .waitForURL((url) => !String(url).includes("/auth"), { timeout: timeoutMs }) .catch(() => {}); return await fn(page); } finally { await context.close().catch(() => {}); } } module.exports = { DEFAULT_EMAIL, DEFAULT_PASSWORD, DEFAULT_USERNAME, DEFAULT_ADMIN_EMAIL, DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME, loginViaAuthForm, loginViaAuthApi, loginAsAdminViaAuthForm, loginAsAdminViaAuthApi, withAdminBrowserSession, };