Files
mnote/scripts/task179-tree-create-delete-no-reload-smoke.js
T
lix-2026 96e03645f7 chore: 收口 review 执行清单与 runtime 验证
- 补齐 design/10-review 执行清单、验收标准与相关设计治理记录

- 迁移已完成的 tree、mindmap、runtime fallback、AI kernel 等设计和缺陷条目

- 推进 Rust Web runtime、tree/sidebar、page aggregate、mindmap 与 OnlyOffice 路由侧验证支撑

- 增加 task177-task180 smoke/audit 脚本及前端相关测试覆盖
2026-05-14 05:52:08 +08:00

194 lines
7.1 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
cleanupDocuments,
createTempDocument,
ensureAuthenticated,
renameDocument,
} = require("./tree-shell-smoke-helpers");
const TASK = "task179-tree-create-delete-no-reload-smoke";
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
async function writeResult(payload) {
await fs.mkdir(OUTPUT_DIR, { recursive: true });
await fs.writeFile(RESULT_PATH, `${JSON.stringify({ ...payload, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8");
}
function fileTreeDocumentRowSelector(documentId) {
return `[data-testid="filetree-doc-row"][data-document-id="${documentId}"], [data-testid="filetree-doc-row"][data-doc-id="${documentId}"]`;
}
function pageTreeDocumentRowSelector(documentId) {
return `.tree-row[data-shell-mode="page"][data-node-id="${documentId}"]`;
}
function documentRowSelector(mode, documentId) {
return mode === "page" ? pageTreeDocumentRowSelector(documentId) : fileTreeDocumentRowSelector(documentId);
}
function renameInputSelector(mode) {
return mode === "page"
? ".tree-rename-input[data-rename-id]"
: ".tree-rename-input[data-rename-id^='doc:']";
}
function documentIdFromRenameId(mode, renameId) {
return mode === "page" ? String(renameId || "") : String(renameId || "").replace(/^doc:/, "");
}
async function readShellState(page) {
return page.evaluate(() => ({
url: window.location.href,
rows: Array.from(document.querySelectorAll(".tree-row[data-shell-mode='filetree']")).map((row) => ({
rowId: row instanceof HTMLElement ? row.dataset.rowId || "" : "",
documentId: row instanceof HTMLElement ? row.dataset.documentId || "" : "",
title: row instanceof HTMLElement ? row.textContent || "" : "",
})),
status: document.getElementById("tree-shell-status")?.textContent || "",
lastAction: document.getElementById("tree-shell-last-action")?.textContent || "",
}));
}
async function main() {
const result = {
ok: false,
task: TASK,
baseUrl: BASE_URL,
createdIds: [],
navigationEvents: [],
treeCommandRequests: [],
modes: {},
};
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
page.on("request", (request) => {
const url = request.url();
if (url.includes("/api/tree/commands")) {
result.treeCommandRequests.push({
method: request.method(),
url,
body: request.postData() || null,
at: Date.now(),
});
}
});
try {
await ensureAuthenticated(page, context.request);
page.on("framenavigated", (frame) => {
if (frame === page.mainFrame()) {
result.navigationEvents.push({ url: frame.url(), at: Date.now() });
}
});
for (const mode of ["filetree", "page"]) {
const root = await createTempDocument(context.request, null);
result.createdIds.push(root.documentId);
await renameDocument(
context.request,
root.workspaceId,
root.documentId,
`task179-tree-${mode}-root-${Date.now().toString().slice(-6)}`,
);
const treeUrl = `${BASE_URL}/tree?workspaceId=${encodeURIComponent(root.workspaceId)}&mode=${encodeURIComponent(mode)}&activeDocumentId=${encodeURIComponent(root.documentId)}`;
await page.goto(treeUrl, { waitUntil: "commit", timeout: UI_TIMEOUT_MS });
await page.locator(documentRowSelector(mode, root.documentId)).waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
const initialUrl = page.url();
const navigationStartIndex = result.navigationEvents.length;
const createStartedAt = Date.now();
await page.getByTestId("tree-create-root").click({ timeout: UI_TIMEOUT_MS });
const renameInput = page.locator(renameInputSelector(mode)).first();
await renameInput.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const renameId = await renameInput.getAttribute("data-rename-id");
const createdDocumentId = documentIdFromRenameId(mode, renameId);
if (!createdDocumentId) throw new Error(`${mode}_created_document_id_missing:${renameId || ""}`);
result.createdIds.push(createdDocumentId);
await page.locator(documentRowSelector(mode, createdDocumentId)).waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
await page.keyboard.press("Escape");
const createFinishedAt = Date.now();
const afterCreateUrl = page.url();
if (afterCreateUrl !== initialUrl) {
throw new Error(`${mode}_create_changed_url:${initialUrl}->${afterCreateUrl}`);
}
const navigationAfterCreate = result.navigationEvents.slice(navigationStartIndex);
if (navigationAfterCreate.length !== 0) {
throw new Error(`${mode}_create_triggered_navigation:${JSON.stringify(navigationAfterCreate)}`);
}
let deleteMs = null;
let afterDeleteUrl = page.url();
let navigationAfterDelete = result.navigationEvents.slice(navigationStartIndex);
if (mode === "filetree") {
const deleteStartedAt = Date.now();
const createdRow = page.locator(documentRowSelector(mode, createdDocumentId)).first();
await createdRow.click({ timeout: UI_TIMEOUT_MS });
await page.keyboard.press("Delete");
const confirmButton = page.locator('.tree-preflight-actions button[data-role="confirm"]').first();
await confirmButton.click({ timeout: UI_TIMEOUT_MS });
await page.locator(documentRowSelector(mode, createdDocumentId)).waitFor({
state: "detached",
timeout: UI_TIMEOUT_MS,
});
const deleteFinishedAt = Date.now();
deleteMs = deleteFinishedAt - deleteStartedAt;
afterDeleteUrl = page.url();
if (afterDeleteUrl !== initialUrl) {
throw new Error(`${mode}_delete_changed_url:${initialUrl}->${afterDeleteUrl}`);
}
navigationAfterDelete = result.navigationEvents.slice(navigationStartIndex);
if (navigationAfterDelete.length !== 0) {
throw new Error(`${mode}_delete_triggered_navigation:${JSON.stringify(navigationAfterDelete)}`);
}
}
result.modes[mode] = {
timings: {
createMs: createFinishedAt - createStartedAt,
deleteMs,
},
initialUrl,
finalUrl: afterDeleteUrl,
createdDocumentId,
navigationEvents: navigationAfterDelete,
finalState: await readShellState(page),
};
}
result.ok = true;
await writeResult(result);
} catch (error) {
result.error = error instanceof Error ? error.stack || error.message : String(error);
result.failureState = await readShellState(page).catch(() => null);
await writeResult(result);
throw error;
} finally {
await cleanupDocuments(context.request, result.createdIds).catch(() => undefined);
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});