211 lines
7.5 KiB
JavaScript
211 lines
7.5 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");
|
|
|
|
if (process.env.MNOTE_ALLOW_DEBUG_TREE_SMOKE !== "1") {
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
skipped: true,
|
|
task: TASK,
|
|
reason:
|
|
"task179 只覆盖已退为显式 debug/internal 的 /tree 壳;默认跳过,避免干扰 local-first 主链回归。设置 MNOTE_ALLOW_DEBUG_TREE_SMOKE=1 后可显式复核。",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
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;
|
|
});
|