"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"); 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 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 }); 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 }, ); await searchInput.focus(); await page.waitForFunction( () => { const active = document.activeElement; return active instanceof HTMLInputElement && active.placeholder === "移动到..."; }, undefined, { timeout: UI_TIMEOUT_MS }, ); const readFocusedPickerItemKey = async () => 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)) { return ""; } const rootButton = frame.contentDocument?.querySelector( '.tree-row[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") || "" : ""; }); const moveHighlightTo = async (targetNodeId, maxSteps = 4) => { 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)) { return false; } const rootButton = frame.contentDocument?.querySelector( '.tree-row[data-testid="tree-picker-root"][data-focused="true"]', ); if (rootButton) { return true; } return Boolean(frame.contentDocument?.querySelector('.tree-row[data-focused="true"]')); }, undefined, { 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) => { 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; } const rootButton = frame.contentDocument?.querySelector( '.tree-row[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; }, currentKey, { 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); });