"use strict"; // 说明: // - 这份 smoke 只验证 rust_family picker 的键盘高亮与 Enter 选中。 // - 这里刻意使用 fresh-open 的最短链路,避免把空态/多轮搜索切换的抖动混进同一条回归里。 const { chromium } = require("playwright"); const { UI_TIMEOUT_MS, createTempDocument, ensureAuthenticated, ensurePageOptionsVisible, openDocument, purgeDocument, renameDocument, } = require("./tree-shell-smoke-helpers"); const ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST = String(process.env.NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST || "").trim() === "1"; function treeHostImplementationUsesIframe(implementation) { return ( implementation === "mnote_web_iframe_proxy" || implementation === "rust_inline_compat_host" || implementation === "rust_runtime_artifact_host" ); } async function waitForPickerShellReady(page) { await page.waitForFunction( () => { const host = document.querySelector('[data-testid="tree-picker-surface"]'); if (!(host instanceof HTMLElement) || host.getClientRects().length === 0) { return false; } const implementation = host.getAttribute("data-tree-host-implementation") || ""; if ( implementation === "mnote_web_iframe_proxy" || implementation === "rust_inline_compat_host" || implementation === "rust_runtime_artifact_host" ) { const iframe = host.querySelector('[data-testid="tree-picker-surface-rust-iframe"]'); if (!(iframe instanceof HTMLIFrameElement) || iframe.getClientRects().length === 0) { return false; } const doc = iframe.contentDocument; const status = doc?.querySelector("#tree-shell-status")?.textContent ?? ""; return Boolean(doc?.querySelector("#tree-shell-state")) && status.includes("就绪"); } const domHost = host.querySelector('[data-testid="tree-picker-surface-dom-host"]'); return ( domHost instanceof HTMLElement && domHost.getClientRects().length > 0 && domHost.getAttribute("data-tree-browser-bridge") === "dom_wasm" && domHost.getAttribute("data-tree-dom-shell-ready") === "true" ); }, undefined, { timeout: UI_TIMEOUT_MS }, ); } async function pickerUsesLegacyIframe(pickerSurface) { const implementation = await pickerSurface.getAttribute("data-tree-host-implementation"); if (treeHostImplementationUsesIframe(implementation) && !ALLOW_LEGACY_TREE_SHELL_IFRAME_HOST) { throw new Error( `tree-picker-surface 默认主路径不能再使用 legacy iframe host: ${implementation || "unknown"}`, ); } return ( treeHostImplementationUsesIframe(implementation) && (await pickerSurface.locator('[data-testid="tree-picker-surface-rust-iframe"]').count()) > 0 ); } async function main() { const headless = process.env.MNOTE_SMOKE_HEADLESS === "1" ? true : process.env.MNOTE_SMOKE_HEADLESS === "0" ? false : !process.env.DISPLAY; const browser = await chromium.launch({ headless }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, }); const page = await context.newPage(); let fixture = null; let caughtError = null; try { await ensureAuthenticated(page, context.request); const uniqueSuffix = Date.now().toString(); const parent = await createTempDocument(context.request, null); const childA = await createTempDocument(context.request, parent.documentId); const target = await createTempDocument(context.request, null); fixture = { workspaceId: parent.workspaceId, parentId: parent.documentId, childAId: childA.documentId, targetId: target.documentId, parentTitle: `task113-parent-${uniqueSuffix}`, childATitle: `task113-child-a-${uniqueSuffix}`, targetTitle: `task113-target-${uniqueSuffix}`, createdIds: [parent.documentId, childA.documentId, target.documentId], }; await renameDocument(context.request, fixture.workspaceId, fixture.parentId, fixture.parentTitle); await renameDocument(context.request, fixture.workspaceId, fixture.childAId, fixture.childATitle); await renameDocument(context.request, fixture.workspaceId, fixture.targetId, fixture.targetTitle); await openDocument(page, fixture.workspaceId, fixture.childAId); await ensurePageOptionsVisible(page); const openButton = page.getByRole("button", { name: "移动/嵌入到..." }); await openButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await openButton.click({ timeout: UI_TIMEOUT_MS }); const dialog = page.getByRole("dialog"); await dialog.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); const pickerSurface = dialog.getByTestId("tree-picker-surface"); await pickerSurface.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); await waitForPickerShellReady(page); const usesLegacyIframe = await pickerUsesLegacyIframe(pickerSurface); const searchInput = dialog.getByPlaceholder("移动到..."); const keyboardQuery = `task113-keyboard-${Date.now()}`; await page.route("**/api/search/documents", async (route) => { const payload = route.request().postDataJSON(); if (payload?.query !== keyboardQuery) { await route.continue(); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ results: [ { id: fixture.parentId, title: fixture.parentTitle, matchField: "title", }, { id: fixture.targetId, title: fixture.targetTitle, matchField: "title", }, ], }), }); }); try { await searchInput.fill(keyboardQuery, { timeout: UI_TIMEOUT_MS }); if (usesLegacyIframe) { await page.waitForFunction( (nodeId) => { const frame = Array.from( document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'), ) .reverse() .find((element) => element instanceof HTMLIFrameElement) ?? null; if (!(frame instanceof HTMLIFrameElement)) { return false; } return Boolean(frame.contentDocument?.querySelector(`.tree-row[data-node-id="${nodeId}"]`)); }, fixture.targetId, { timeout: UI_TIMEOUT_MS }, ); } else { await dialog .locator(`[data-testid="tree-picker-row"][data-node-id="${fixture.targetId}"]`) .waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); } await searchInput.focus(); await page.waitForFunction( () => { const active = document.activeElement; return active instanceof HTMLInputElement && active.placeholder === "移动到..."; }, undefined, { timeout: UI_TIMEOUT_MS }, ); const readFocusedPickerItemKey = async () => usesLegacyIframe ? page.evaluate(() => { const frame = Array.from( document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'), ) .reverse() .find((element) => element instanceof HTMLIFrameElement) ?? null; if (!(frame instanceof HTMLIFrameElement) || !frame.contentDocument) { return ""; } const rootButton = frame.contentDocument.querySelector( '[data-testid="tree-picker-root"][data-focused="true"]', ); if (rootButton) { return "__root__"; } const row = frame.contentDocument.querySelector('.tree-row[data-focused="true"]'); return row ? row.getAttribute("data-node-id") || "" : ""; }) : dialog.evaluate((dialogElement) => { const rootButton = dialogElement.querySelector('[data-testid="tree-picker-root"][data-focused="true"]'); if (rootButton) { return "__root__"; } const row = dialogElement.querySelector('.tree-row[data-focused="true"]'); return row ? row.getAttribute("data-node-id") || "" : ""; }); const moveHighlightTo = async (targetNodeId, maxSteps = 4) => { if (usesLegacyIframe) { await page.waitForFunction( () => { const frame = Array.from( document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'), ) .reverse() .find((element) => element instanceof HTMLIFrameElement) ?? null; if (!(frame instanceof HTMLIFrameElement) || !frame.contentDocument) { return false; } const rootButton = frame.contentDocument.querySelector( '[data-testid="tree-picker-root"][data-focused="true"]', ); const row = frame.contentDocument.querySelector('.tree-row[data-focused="true"]'); return Boolean(rootButton || row); }, undefined, { timeout: UI_TIMEOUT_MS }, ); } else { await dialog .locator('[data-testid="tree-picker-root"][data-focused="true"], .tree-row[data-focused="true"]') .first() .waitFor({ state: "attached", timeout: UI_TIMEOUT_MS }); } for (let index = 0; index < maxSteps; index += 1) { const currentKey = await readFocusedPickerItemKey(); if (currentKey === targetNodeId) { return; } await page.keyboard.press("ArrowDown"); await page.waitForFunction( ({ previousKey, legacyIframe }) => { if (legacyIframe) { const frame = Array.from( document.querySelectorAll('[data-testid="tree-picker-surface-rust-iframe"]'), ) .reverse() .find((element) => element instanceof HTMLIFrameElement) ?? null; if (!(frame instanceof HTMLIFrameElement) || !frame.contentDocument) { return false; } const rootButton = frame.contentDocument.querySelector( '[data-testid="tree-picker-root"][data-focused="true"]', ); const row = frame.contentDocument.querySelector('.tree-row[data-focused="true"]'); const currentKey = rootButton ? "__root__" : row ? row.getAttribute("data-node-id") || "" : ""; return currentKey !== previousKey; } const dialogElement = document.querySelector('[role="dialog"]'); if (!(dialogElement instanceof HTMLElement)) { return false; } const rootButton = dialogElement.querySelector('[data-testid="tree-picker-root"][data-focused="true"]'); const row = dialogElement.querySelector('.tree-row[data-focused="true"]'); const currentKey = rootButton ? "__root__" : row ? row.getAttribute("data-node-id") || "" : ""; return currentKey !== previousKey; }, { previousKey: currentKey, legacyIframe: usesLegacyIframe }, { timeout: UI_TIMEOUT_MS }, ); await page.waitForFunction( () => { const active = document.activeElement; return active instanceof HTMLInputElement && active.placeholder === "移动到..."; }, undefined, { timeout: UI_TIMEOUT_MS }, ); } const finalKey = await readFocusedPickerItemKey(); if (finalKey !== targetNodeId) { throw new Error(`picker 键盘高亮未落到目标节点:当前=${finalKey || ""} 目标=${targetNodeId}`); } }; await moveHighlightTo(fixture.targetId); const moveRequest = page.waitForResponse( async (response) => { if ( !response.url().includes("/api/tree/commands") || response.request().method() !== "POST" || !response.ok() ) { return false; } const payload = response.request().postDataJSON(); return payload?.action === "move" && payload?.documentId === fixture.childAId && payload?.parentId === fixture.targetId; }, { timeout: UI_TIMEOUT_MS }, ); await searchInput.focus(); await page.keyboard.press("Enter"); await moveRequest; await dialog.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS }); } finally { await page.unroute("**/api/search/documents").catch(() => undefined); } console.log( JSON.stringify( { ok: true, workspaceId: fixture.workspaceId, sourceId: fixture.childAId, targetId: fixture.targetId, searchQuery: keyboardQuery, pickerKeyboardHighlight: true, pickerKeyboardSelect: true, }, null, 2, ), ); } catch (error) { caughtError = error; } finally { if (fixture) { for (const documentId of [...fixture.createdIds].reverse()) { try { await purgeDocument(context.request, documentId); } catch { // 说明:移动后父级与目标级的清理顺序可能变化,这里忽略重复清理错误。 } } } await page.close().catch(() => undefined); await context.close().catch(() => undefined); await browser.close().catch(() => undefined); } if (caughtError) { throw caughtError; } } main().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : String(error)); process.exit(1); });