4-26 树rust-2

This commit is contained in:
lix-2026
2026-04-26 04:29:23 +08:00
parent 94631f3636
commit 338bb2e20f
58 changed files with 11718 additions and 1256 deletions
+206 -21
View File
@@ -6,6 +6,8 @@ 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 = "测试用户";
const TEST_EMAIL = "test@example.com";
const TEST_PASSWORD = "Test123456";
function assert(condition, message) {
if (!condition) {
@@ -65,15 +67,33 @@ async function requestJson(requestContext, path, init = {}) {
}
async function createTempDocument(requestContext, parentId = null) {
const payload = await requestJson(requestContext, "/api/documents/create", {
const payload = await requestJson(requestContext, "/api/tree/commands", {
method: "POST",
data: { parentId },
data: {
action: "create",
parentId,
},
});
assert(payload && typeof payload.id === "string", "创建临时页面失败:缺少 id");
assert(payload && typeof payload.workspace_id === "string", "创建临时页面失败:缺少 workspace_id");
const result = payload && typeof payload.result === "object" ? payload.result : null;
const documentId =
result && typeof result.documentId === "string"
? result.documentId
: payload && typeof payload.id === "string"
? payload.id
: "";
const workspaceId =
result && typeof result.workspaceId === "string"
? result.workspaceId
: payload && typeof payload.workspace_id === "string"
? payload.workspace_id
: payload && typeof payload.workspaceId === "string"
? payload.workspaceId
: "";
assert(documentId, "创建临时页面失败:缺少 documentId/id");
assert(workspaceId, "创建临时页面失败:缺少 workspaceId/workspace_id");
return {
documentId: payload.id,
workspaceId: payload.workspace_id,
documentId,
workspaceId,
};
}
@@ -101,6 +121,23 @@ async function getViewerIdentity(requestContext) {
return payload;
}
async function tryApiQuickLogin(requestContext) {
await requestJson(requestContext, "/api/auth", {
method: "POST",
data: {
action: "auth:signIn",
args: {
provider: "password",
params: {
email: TEST_EMAIL,
password: TEST_PASSWORD,
flow: "signIn",
},
},
},
});
}
async function waitForAuthenticatedRedirect(page, timeout = 8_000) {
await page.waitForURL((url) => !url.toString().includes("/auth"), {
timeout,
@@ -110,7 +147,16 @@ async function waitForAuthenticatedRedirect(page, timeout = 8_000) {
async function isVisible(locator) {
try {
return await locator.isVisible();
return await locator.evaluateAll((elements) =>
elements.some((element) => {
if (!(element instanceof HTMLElement)) {
return false;
}
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
}),
);
} catch {
return false;
}
@@ -139,11 +185,11 @@ async function registerTestAccountIfNeeded(page) {
}
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="email"]').fill(TEST_EMAIL, { 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.locator('input[name="password"]').fill(TEST_PASSWORD, { timeout: UI_TIMEOUT_MS });
await page.getByRole("button", { name: "注册" }).click({ timeout: UI_TIMEOUT_MS });
try {
await waitForAuthenticatedRedirect(page, UI_TIMEOUT_MS);
@@ -166,21 +212,37 @@ async function waitForViewerIdentity(requestContext, attempts = 6) {
throw lastError instanceof Error ? lastError : new Error("获取当前用户失败");
}
async function ensureAuthenticated(page, requestContext) {
async function ensureAuthenticatedViaUi(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 }),
]);
const deadline = Date.now() + UI_TIMEOUT_MS;
while (page.url().includes("/auth") && Date.now() < deadline) {
if (await isVisible(quickLoginButton)) {
break;
}
await page.waitForTimeout(200);
}
if (!page.url().includes("/auth")) {
return await waitForViewerIdentity(requestContext);
}
if (!(await isVisible(quickLoginButton))) {
await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS }).catch(() => undefined);
const retryDeadline = Date.now() + Math.min(8_000, UI_TIMEOUT_MS);
while (page.url().includes("/auth") && Date.now() < retryDeadline) {
if (await isVisible(quickLoginButton)) {
break;
}
await page.waitForTimeout(200);
}
}
if (!page.url().includes("/auth")) {
return await waitForViewerIdentity(requestContext);
}
if (!(await isVisible(quickLoginButton))) {
throw new Error("认证页未出现测试账号快速登录按钮");
}
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
try {
await waitForAuthenticatedRedirect(page, UI_TIMEOUT_MS);
@@ -198,6 +260,36 @@ async function ensureAuthenticated(page, requestContext) {
return await waitForViewerIdentity(requestContext);
}
async function ensureAuthenticated(page, requestContext) {
const existingViewer = await waitForViewerIdentity(requestContext, 1).catch(() => null);
if (existingViewer) {
return existingViewer;
}
const preferUiAuth = Boolean(process.env.DISPLAY);
if (preferUiAuth) {
try {
return await ensureAuthenticatedViaUi(page, requestContext);
} catch {
// 说明:有图形环境时优先走真实 UI 登录;如果异常,再回退到 API 登录。
}
}
const apiViewer = await (async () => {
try {
await tryApiQuickLogin(requestContext);
return await waitForViewerIdentity(requestContext, 3);
} catch {
return null;
}
})();
if (apiViewer) {
return apiViewer;
}
return await ensureAuthenticatedViaUi(page, requestContext);
}
async function prepareTempTreeFixture(requestContext) {
const uniqueSuffix = Date.now().toString();
const parentTitle = `task-tree-parent-${uniqueSuffix}`;
@@ -228,28 +320,121 @@ async function cleanupDocuments(requestContext, createdIds) {
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 });
const hasSidebarControls = async () => {
const groupButton = page.getByRole("button", { name: "分组" });
const fileButton = page.getByRole("button", { name: "文件" });
return (await isVisible(groupButton)) || (await isVisible(fileButton));
};
const gotoDocument = async () => {
try {
await page.goto(url, { waitUntil: "commit", timeout: UI_TIMEOUT_MS });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes("ERR_ABORTED")) {
throw error;
}
await page.waitForTimeout(500);
await page.goto(url, { waitUntil: "commit", timeout: UI_TIMEOUT_MS });
}
};
await gotoDocument();
const firstDeadline = Date.now() + Math.min(UI_TIMEOUT_MS, 10_000);
while (Date.now() < firstDeadline) {
if (await hasSidebarControls()) {
return url;
}
await page.waitForTimeout(250);
}
await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS }).catch(() => undefined);
const secondDeadline = Date.now() + Math.min(UI_TIMEOUT_MS, 10_000);
while (Date.now() < secondDeadline) {
if (await hasSidebarControls()) {
return url;
}
await page.waitForTimeout(250);
}
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 });
const waitForHost = () =>
page.waitForFunction(
() => {
const pageHost = document.querySelector('[data-testid="sidebar-page-tree-shell"]');
return pageHost instanceof HTMLElement && pageHost.getClientRects().length > 0;
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
for (let attempt = 0; attempt < 2; attempt += 1) {
await button.click({ timeout: UI_TIMEOUT_MS });
try {
await waitForHost();
return;
} catch (error) {
if (attempt === 1) {
throw error;
}
}
}
}
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 });
const waitForHost = () =>
page.waitForFunction(
() => {
const fileHost = document.querySelector('[data-testid="sidebar-file-tree-shell"]');
return fileHost instanceof HTMLElement && fileHost.getClientRects().length > 0;
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
for (let attempt = 0; attempt < 2; attempt += 1) {
await button.click({ timeout: UI_TIMEOUT_MS });
try {
await waitForHost();
return;
} catch (error) {
if (attempt === 1) {
throw error;
}
}
}
}
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")) || "";
const readLabel = async () => (await toggle.getAttribute("aria-label")) || "";
const waitUntilExpanded = async () => {
await page.waitForFunction(
() => {
const buttons = Array.from(document.querySelectorAll("button"));
return buttons.some((button) => (button.getAttribute("aria-label") || "").includes("隐藏页面选项"));
},
undefined,
{ timeout: UI_TIMEOUT_MS },
);
};
const label = await readLabel();
if (label.includes("显示")) {
await toggle.click({ timeout: UI_TIMEOUT_MS });
try {
await waitUntilExpanded();
} catch {
await toggle.click({ timeout: UI_TIMEOUT_MS });
await waitUntilExpanded();
}
}
}