Files
mnote/scripts/task442-trash-modal-workbench-smoke.js
T

199 lines
9.2 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
"use strict";
const { loginViaAuthForm } = require('./lib/browser-auth-login');
const assert = require("node:assert");
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");
const ROOT = path.resolve(__dirname, "..");
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
const OUT_DIR = path.join(ROOT, "tmp", "task442-trash-modal-workbench-smoke");
async function quickLogin(page) {
// 7-76 P0: 标准表单登录(无测试快速登录按钮)
const base =
(typeof BASE_URL !== "undefined" && BASE_URL) ||
(typeof baseUrl !== "undefined" && baseUrl) ||
process.env.MNOTE_UI_BASE_URL ||
"http://127.0.0.1:3000";
const timeout =
(typeof UI_TIMEOUT_MS !== "undefined" && UI_TIMEOUT_MS) ||
(typeof TIMEOUT !== "undefined" && TIMEOUT) ||
30_000;
if (!String(page.url() || "").includes("/auth")) {
await page.goto(String(base).replace(/\/+$/, "") + "/auth", {
waitUntil: "commit",
timeout,
});
}
await loginViaAuthForm(page, {
baseUrl: base,
timeoutMs: timeout,
gotoAuth: false,
});
await page
.waitForURL((url) => !String(url).includes("/auth"), { timeout })
.catch(() => {});
}
async function readState(page) {
return page.evaluate(() => {
const fileRoot = document.getElementById("sidebar-file-tree-root");
const activeRow = document.querySelector("#sidebar-file-tree-root .tree-row[data-selected='true'], #sidebar-tree-root .tree-row[data-active='true']");
const modal = document.querySelector('[data-testid="mnote-trash-modal"]');
const panel = document.querySelector('[data-testid="mnote-trash-modal"] .mnote-trash-modal__panel');
const workbench = document.querySelector('[data-testid="mnote-trash-modal"] [data-testid="mnote-trash-workbench"]');
const modalRect = modal instanceof HTMLElement ? modal.getBoundingClientRect() : null;
const panelRect = panel instanceof HTMLElement ? panel.getBoundingClientRect() : null;
return {
url: window.location.href,
pathname: window.location.pathname,
modalOpen: modal instanceof HTMLElement,
workbenchVisible: workbench instanceof HTMLElement,
panelRect: panelRect ? {
left: panelRect.left,
right: panelRect.right,
top: panelRect.top,
bottom: panelRect.bottom,
width: panelRect.width,
height: panelRect.height,
} : null,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
modalRect: modalRect ? {
left: modalRect.left,
right: modalRect.right,
top: modalRect.top,
bottom: modalRect.bottom,
} : null,
fileScrollTop: fileRoot instanceof HTMLElement ? fileRoot.scrollTop : null,
activeRowId: activeRow instanceof HTMLElement ? activeRow.getAttribute("data-row-id") || activeRow.getAttribute("data-node-id") || "" : "",
bodyShell: document.body.getAttribute("data-mnote-shell") || "",
modalRole: modal instanceof HTMLElement ? modal.getAttribute("role") || "" : "",
};
});
}
async function requestJson(request, pathName, data) {
const response = await request.fetch(`${BASE_URL}${pathName}`, {
method: "POST",
headers: { "content-type": "application/json" },
data,
timeout: UI_TIMEOUT_MS,
});
const payload = await response.json().catch(() => null);
if (!response.ok()) {
throw new Error(`${pathName} failed ${response.status()}: ${JSON.stringify(payload)}`);
}
return payload;
}
async function treeCommand(request, body) {
const payload = await requestJson(request, "/api/tree/commands", body);
assert(payload?.result, `tree command 缺少 result: ${JSON.stringify(payload)}`);
return payload.result;
}
async function readWorkspaceId(page) {
const workspaceId = await page.evaluate(() => {
const node = document.querySelector("#sidebar-file-tree-root[data-workspace-id], #sidebar-tree-root[data-workspace-id], [data-workspace-id]");
return node instanceof HTMLElement ? node.getAttribute("data-workspace-id") || "" : "";
});
assert(workspaceId && workspaceId !== "default" && !workspaceId.startsWith("local:"), `缺少真实云空间 workspaceId: ${workspaceId}`);
return workspaceId;
}
async function main() {
await fs.mkdir(OUT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: process.env.HEADFUL !== "1" });
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
const page = await context.newPage();
const requests = [];
page.on("request", (request) => {
if (request.url().includes("/trash")) requests.push({ url: request.url(), method: request.method() });
});
const result = { ok: false, baseUrl: BASE_URL, workspaceId: null, archivedDocumentId: null, before: null, afterOpen: null, afterRestore: null, afterClose: null, requests };
try {
await quickLogin(page);
await page.goto(BASE_URL, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.locator('[data-testid="wolai-sidebar"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
result.workspaceId = await readWorkspaceId(page);
const created = await treeCommand(context.request, {
action: "create",
workspaceId: result.workspaceId,
parentId: null,
title: `TRASH-MODAL-${Date.now().toString().slice(-6)}`,
});
result.archivedDocumentId = created.documentId;
assert(result.archivedDocumentId, `创建临时页面缺少 documentId: ${JSON.stringify(created)}`);
await treeCommand(context.request, {
action: "archive",
workspaceId: result.workspaceId,
documentId: result.archivedDocumentId,
});
result.before = await readState(page);
const trashEntry = page.locator('[data-testid="mnote-sidebar-trash-entry"], .wolai-sidebar-footer .wolai-footer-entry[href="/trash"]').first();
await trashEntry.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await trashEntry.click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-testid="mnote-trash-modal"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.locator('[data-testid="mnote-trash-modal"] [data-testid="mnote-trash-workbench"]').waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
result.afterOpen = await readState(page);
assert.equal(result.afterOpen.url, result.before.url, "点击垃圾箱应打开弹窗,不应离开当前 URL");
assert.equal(result.afterOpen.modalOpen, true, "应出现垃圾箱弹窗");
assert.equal(result.afterOpen.workbenchVisible, true, "弹窗内应复用 mnote-trash-workbench");
assert.equal(result.afterOpen.modalRole, "dialog", "垃圾箱弹窗应使用 dialog role");
assert(result.afterOpen.panelRect, "垃圾箱弹窗应包含面板");
assert(Math.abs((result.afterOpen.panelRect.left + result.afterOpen.panelRect.right) / 2 - result.afterOpen.viewportWidth / 2) <= 8,
`垃圾箱面板应居中显示: ${JSON.stringify(result.afterOpen.panelRect)}`);
assert(result.afterOpen.panelRect.top > 20 && result.afterOpen.panelRect.bottom < result.afterOpen.viewportHeight - 20,
`垃圾箱面板应保留上下留白: ${JSON.stringify(result.afterOpen.panelRect)}`);
const trashRow = page.locator(`[data-testid="mnote-trash-modal"] [data-trash-row="document"][data-document-id="${result.archivedDocumentId}"]`);
await trashRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await trashRow.locator('[data-trash-action="restore"]').click({ timeout: UI_TIMEOUT_MS });
await trashRow.waitFor({ state: "detached", timeout: UI_TIMEOUT_MS });
result.afterRestore = await readState(page);
assert.equal(result.afterRestore.url, result.before.url, "弹窗内恢复页面后仍应停留在原 URL");
await page.locator('[data-testid="mnote-trash-modal-close"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-testid="mnote-trash-modal"]').waitFor({ state: "detached", timeout: UI_TIMEOUT_MS });
result.afterClose = await readState(page);
assert.equal(result.afterClose.url, result.before.url, "关闭垃圾箱弹窗后仍应停留在原 URL");
assert.equal(result.afterClose.fileScrollTop, result.before.fileScrollTop, "关闭弹窗后 File Tree scrollTop 不应被无关重置");
assert.equal(result.afterClose.activeRowId, result.before.activeRowId, "关闭弹窗后 active/selected row 不应被无关重置");
result.ok = true;
} finally {
if (result.workspaceId && result.archivedDocumentId) {
await treeCommand(context.request, {
action: "archive",
workspaceId: result.workspaceId,
documentId: result.archivedDocumentId,
}).catch(() => {});
await treeCommand(context.request, {
action: "purge",
workspaceId: result.workspaceId,
documentId: result.archivedDocumentId,
}).catch(() => {});
}
await fs.writeFile(path.join(OUT_DIR, "result.json"), `${JSON.stringify(result, null, 2)}\n`, "utf8");
await browser.close().catch(() => {});
}
console.log(JSON.stringify(result, null, 2));
}
main().catch((error) => {
console.error(error);
process.exit(1);
});