#!/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; });