"use strict"; const assert = require("node:assert/strict"); const { chromium } = require("playwright"); const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const CONVEX_URL = (process.env.NEXT_PUBLIC_CONVEX_URL || process.env.CONVEX_SELF_HOSTED_URL || "http://127.0.0.1:3210").replace(/\/+$/, ""); const e2ePassword = () => process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join(""); // 说明:该 smoke 覆盖 convex-source trash 兼容路径,不代表 local-first 默认文件回收策略。 async function requestJson(request, path, init = {}) { const response = await request.fetch(`${BASE_URL}${path}`, { ...init, headers: { ...(init.data !== undefined ? { "content-type": "application/json" } : {}), ...(init.headers || {}), }, timeout: 20_000, }); 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()} ${typeof payload === "string" ? payload : JSON.stringify(payload)}`); } return payload; } async function convexCall(context, kind, path, args) { const cookies = await context.cookies(BASE_URL); const jwt = cookies.find((cookie) => cookie.name === "__convexAuthJWT")?.value; assert(jwt, "缺少 __convexAuthJWT cookie"); const response = await fetch(`${CONVEX_URL}/api/${kind}`, { method: "POST", headers: { authorization: `Bearer ${jwt}`, "content-type": "application/json", "Convex-Client": "mnote-trash-empty-smoke", }, body: JSON.stringify({ path, format: "convex_encoded_json", args: [args] }), }); const body = await response.json(); if (!response.ok || body.status !== "success") { throw new Error(`Convex ${kind} ${path} 失败: ${response.status} ${JSON.stringify(body)}`); } return body.value; } async function main() { const stamp = Date.now(); const email = `mnote.trash.${stamp}@example.com`; const username = `trash-${stamp}`; const assetId = `asset_trash_${stamp}`; const mindmapId = `mind_trash_${stamp}`; const browser = await chromium.launch({ headless: true }); const context = await browser.newContext(); const page = await context.newPage(); const request = context.request; try { await requestJson(request, "/api/auth", { method: "POST", data: { action: "auth:signIn", args: { provider: "password", params: { email, password: e2ePassword(), flow: "signUp", name: username }, }, }, }); await page.goto(`${BASE_URL}/`, { waitUntil: "domcontentloaded", timeout: 20_000 }); const currentUser = await convexCall(context, "query", "users:currentUser", {}); const userId = currentUser?._id; assert(userId, "缺少 Convex currentUser._id"); const workspaces = await convexCall(context, "query", "workspaces:fetchWorkspaceSummaries", {}); const workspaceId = workspaces.activeWorkspaceId; assert(workspaceId, "缺少隔离 workspaceId"); const root = await requestJson(request, "/api/tree/commands", { method: "POST", data: { action: "create", workspaceId, title: `TEST-10REVIEW-07-P3-root-${stamp}` }, }); const rootId = root.result.documentId; const child = await requestJson(request, "/api/tree/commands", { method: "POST", data: { action: "create", workspaceId, parentId: rootId, title: `TEST-10REVIEW-07-P3-child-${stamp}` }, }); const childId = child.result.documentId; const resourceDoc = await requestJson(request, "/api/tree/commands", { method: "POST", data: { action: "create", workspaceId, title: `TEST-10REVIEW-07-P3-resources-${stamp}` }, }); const resourceDocId = resourceDoc.result.documentId; await convexCall(context, "mutation", "mediaAssets:create", { userId, asset: { id: assetId, workspace_id: workspaceId, document_id: resourceDocId, asset_type: "file", file_url: null, thumbnail_url: null, storage_id: null, bucket: null, storage_path: null, file_name: `TEST-10REVIEW-07-P3-file-${stamp}.txt`, file_size: 12, mime_type: "text/plain", }, }); await convexCall(context, "mutation", "mindmaps:put", { docId: resourceDocId, mindmapId, data: { root: { data: { text: `TEST-10REVIEW-07-P3-mind-${stamp}` }, children: [] } }, createOnly: true, }); const table = await convexCall(context, "mutation", "tables:create", { userId, workspaceId, documentId: resourceDocId, title: `TEST-10REVIEW-07-P3-table-${stamp}`, schema: {}, snapshot: null, }); const tableId = table.id; await convexCall(context, "mutation", "tables:update", { userId, tableId, rows: [{ cells: ["p3-row"] }], }); await requestJson(request, "/api/tree/commands", { method: "POST", data: { action: "archive", workspaceId, documentId: rootId }, }); await requestJson(request, "/api/media/batch", { method: "POST", data: { action: "delete", assetIds: [assetId] }, }); await requestJson(request, `/api/mindmap/${encodeURIComponent(resourceDocId)}/${encodeURIComponent(mindmapId)}`, { method: "DELETE", }); await requestJson(request, `/api/tables/${encodeURIComponent(tableId)}`, { method: "DELETE" }); const trashedDocs = await convexCall(context, "query", "documents:listTrashedByWorkspace", { workspaceId }); assert(trashedDocs.some((doc) => doc.id === rootId), "根页面未进入垃圾箱"); assert(trashedDocs.some((doc) => doc.id === childId), "子页面未级联进入垃圾箱"); const mediaBefore = await convexCall(context, "query", "mediaAssets:getById", { userId, id: assetId }); assert(mediaBefore?.deleted_at, "附件未进入垃圾箱"); const mindmapsBefore = await convexCall(context, "query", "mindmaps:listByWorkspace", { workspaceId, includeDeleted: true }); assert(mindmapsBefore.some((row) => row.mindmap_id === mindmapId && row.deleted_at), "mindmap 未进入垃圾箱"); const tablesBefore = await convexCall(context, "query", "tables:listByWorkspaceForSearch", { userId, workspaceId, includeArchived: true, limit: 100, }); assert(tablesBefore.some((row) => row.id === tableId && row.deleted_at), "table 未进入垃圾箱"); const pageEmpty = await requestJson(request, "/api/documents/empty-trash", { method: "POST", data: { workspaceId } }); const mediaEmpty = await requestJson(request, "/api/media/empty-trash", { method: "POST", data: { workspaceId } }); const mindmapEmpty = await requestJson(request, "/api/mindmap-trash/empty", { method: "POST", data: { workspaceId } }); const tableEmpty = await requestJson(request, "/api/tables/empty-trash", { method: "POST", data: { workspaceId } }); assert.equal(await convexCall(context, "query", "documents:getMeta", { id: rootId }), null, "根页面 purge 后仍可查询"); assert.equal(await convexCall(context, "query", "documents:getMeta", { id: childId }), null, "子页面 purge 后仍可查询"); assert.equal(await convexCall(context, "query", "mediaAssets:getById", { userId, id: assetId }), null, "附件 purge 后仍可查询"); const mindmapsAfter = await convexCall(context, "query", "mindmaps:listByWorkspace", { workspaceId, includeDeleted: true }); assert(!mindmapsAfter.some((row) => row.mindmap_id === mindmapId), "mindmap purge 后仍存在"); assert.equal(await convexCall(context, "query", "tables:get", { userId, tableId }), null, "table purge 后仍可查询"); assert.equal((await convexCall(context, "query", "tables:getRows", { tableId })).length, 0, "table rows purge 后仍存在"); await requestJson(request, "/api/documents/purge", { method: "POST", data: { documentId: resourceDocId } }).catch(() => null); console.log(JSON.stringify({ ok: true, email, userId, workspaceId, rootId, childId, resourceDocId, assetId, mindmapId, tableId, pageEmpty: pageEmpty.result, mediaEmpty: mediaEmpty.result, mindmapEmpty: mindmapEmpty.result, tableEmpty: tableEmpty.result, }, null, 2)); } finally { await browser.close(); } } main().catch((error) => { console.error(error); process.exit(1); });