提交当前顶层 mnote Git 工作区,范围集中在 04-tree-domain 的 resource/trash/filetree 生命周期、mnote-web resource_trash 路由、Convex/Next 兼容接口、sidebar/file-tree 客户端适配、smoke 脚本与对应设计/bug 记录。 不包含被 ignore 的 design/05-editor-mainline/reference-code/leptos-tiptap 嵌套仓库改动。新增 smoke 的测试密码改为运行时读取 MNOTE_E2E_PASSWORD,避免提交明文 credential assignment。
206 lines
8.2 KiB
JavaScript
206 lines
8.2 KiB
JavaScript
"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:3001").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("");
|
|
|
|
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);
|
|
});
|