From 769ef389afdf0f6a2d618599386fa29904a8963c Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Sun, 14 Jun 2026 00:32:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=207-61=20browser=20smoke=20=E2=80=94=20mis?= =?UTF-8?q?sing=20setActiveExtensionsSubtab=20binding,=20brace=20closure,?= =?UTF-8?q?=20add=20smoke=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix sidebar-tree-runtime.js: add missing setActiveExtensionsSubtab binding from settings runtime - fix sidebar-tree-runtime.js: add missing } closing if(pageSettingsTab) block - add scripts/task561-yuxi-extensions-dashboard-smoke.js covering 4 scenarios: 1) extensions tab with 4 sub-panels (知识库/Skills/MCP/Agent工具) 2) knowledge-rag detail popover with source/search/params tabs + real search + param save 3) dashboard tab with 4 cards + refresh 4) tab persistence after page refresh --- .../mnote-web/browser/sidebar-tree-runtime.js | 4 +- ...task561-yuxi-extensions-dashboard-smoke.js | 313 ++++++++++++++++++ 2 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 scripts/task561-yuxi-extensions-dashboard-smoke.js diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js index 2d5f7f29..585cfed6 100644 --- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js @@ -237,6 +237,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim const removeLocalIndexRange = (...args) => sidebarPageSettings.removeLocalIndexRange(...args); const recordPageHistorySnapshot = (...args) => sidebarPageSettings.recordPageHistorySnapshot(...args); const renderPageSettingsPopover = (...args) => sidebarPageSettings.renderPageSettingsPopover(...args); + const setActiveExtensionsSubtab = (...args) => sidebarPageSettings.setActiveExtensionsSubtab(...args); const setActivePageSettingsTab = (...args) => sidebarPageSettings.setActivePageSettingsTab(...args); const togglePageSettingsPopover = (...args) => sidebarPageSettings.togglePageSettingsPopover(...args); const updatePageSettingsTriggerState = (...args) => sidebarPageSettings.updatePageSettingsTriggerState(...args); @@ -3006,6 +3007,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim e.preventDefault(); setActivePageSettingsTab(pageSettingsTab.getAttribute('data-page-settings-tab') || 'page'); return; + } var extensionsSubtab = closestAction(e.target, '[data-extensions-subtab]'); if (extensionsSubtab) { @@ -3014,8 +3016,6 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim return; } - } - var localIndexAction = closestAction(e.target, '[data-local-index-action]'); if (localIndexAction) { e.preventDefault(); diff --git a/scripts/task561-yuxi-extensions-dashboard-smoke.js b/scripts/task561-yuxi-extensions-dashboard-smoke.js new file mode 100644 index 00000000..e4a08c9a --- /dev/null +++ b/scripts/task561-yuxi-extensions-dashboard-smoke.js @@ -0,0 +1,313 @@ +#!/usr/bin/env node +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const http = require("node:http"); +const net = require("node:net"); +const os = require("node:os"); +const path = require("node:path"); +const { spawn } = require("node:child_process"); +const { chromium } = require("playwright"); + +const ROOT = path.resolve(__dirname, ".."); +const OUT_DIR = process.env.MNOTE_YUXI_SMOKE_OUTPUT_DIR || fs.mkdtempSync(path.join(ROOT, "tmp", "task561-yuxi-extensions-")); +const TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 90_000); +const CHROME = process.env.MNOTE_YUXI_CHROME || "/usr/bin/google-chrome"; +const ACTOR = "mnote-e2e"; +const PASSWORD = process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!"; + +function pickPort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + const port = address && typeof address === "object" ? address.port : 0; + server.close(() => resolve(port)); + }); + server.on("error", reject); + }); +} + +function waitForHttpOk(url, timeoutMs) { + const deadline = Date.now() + timeoutMs; + return new Promise((resolve, reject) => { + const tick = () => { + const request = http.get(url, (response) => { + response.resume(); + if (response.statusCode >= 200 && response.statusCode < 500) return resolve(); + retry(); + }); + request.on("error", retry); + request.setTimeout(1000, () => { request.destroy(); retry(); }); + }; + const retry = () => { + if (Date.now() > deadline) return reject(new Error(`server_not_ready: ${url}`)); + setTimeout(tick, 250); + }; + tick(); + }); +} + +async function signIn(context, baseUrl) { + const response = await context.request.fetch(`${baseUrl}/api/auth`, { + method: "POST", + headers: { "content-type": "application/json" }, + data: { action: "auth:signIn", args: { provider: "password", params: { account: ACTOR, password: PASSWORD, flow: "signIn" } } }, + timeout: TIMEOUT_MS, + }); + const text = await response.text(); + assert(response.ok(), `/api/auth 登录失败: ${response.status()} ${text.slice(0, 500)}`); + const whoami = await context.request.fetch(`${baseUrl}/api/auth/whoami`, { timeout: TIMEOUT_MS }); + return JSON.parse(await whoami.text()); +} + +async function main() { + fs.mkdirSync(OUT_DIR, { recursive: true }); + const port = await pickPort(); + const baseUrl = `http://127.0.0.1:${port}`; + const server = spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], { + cwd: path.join(ROOT, "rust"), + env: { ...process.env, MNOTE_WEB_BIND: `127.0.0.1:${port}`, MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}` }, + stdio: ["ignore", "pipe", "pipe"], + }); + let stderr = ""; + server.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); + const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task561-yuxi-")); + let browser; + const failures = []; + function fail(label, detail) { + failures.push({ label, detail }); + console.error(`[FAIL] ${label}: ${detail}`); + } + try { + await waitForHttpOk(`${baseUrl}/health`, TIMEOUT_MS); + browser = await chromium.launch({ headless: process.env.HEADFUL !== "1", executablePath: fs.existsSync(CHROME) ? CHROME : undefined }); + const context = await browser.newContext({ viewport: { width: 1440, height: 960 } }); + const viewer = await signIn(context, baseUrl); + const actorId = viewer.userId || ACTOR; + const suffix = Date.now().toString(36); + const workspaceId = `local-ws:${actorId}:task561-yuxi-${suffix}`; + const rootUri = `file://${root}`; + const relativePath = `Yuxi-Test-${suffix}.md`; + const documentId = `local-md:${relativePath.replaceAll("/", "~2F")}`; + fs.mkdirSync(path.join(root, ".mnote"), { recursive: true }); + fs.writeFileSync(path.join(root, ".mnote", "workspace.json"), JSON.stringify({ workspaceId, ownerId: actorId }, null, 2), "utf8"); + fs.writeFileSync(path.join(root, relativePath), `# Yuxi 7-61 Smoke Test\n\n${suffix}\n`, "utf8"); + + const page = await context.newPage(); + const url = new URL(`${baseUrl}/documents/${encodeURIComponent(documentId)}`); + url.searchParams.set("sourceKind", "local_folder"); + url.searchParams.set("rootUri", rootUri); + url.searchParams.set("workspaceId", workspaceId); + await page.goto(url.toString(), { waitUntil: "domcontentloaded", timeout: TIMEOUT_MS }); + + // ========== SCENARIO 1: 扩展 tab ========== + console.log("=== Scenario 1: 扩展 tab ==="); + const settingsTrigger = page.locator('[data-testid="wolai-page-settings-trigger"]'); + await settingsTrigger.click({ timeout: TIMEOUT_MS }); + await page.waitForSelector('[data-testid="wolai-page-settings-popover"]', { timeout: TIMEOUT_MS }); + + // Click extensions tab + const extTab = page.locator('[data-page-settings-tab="extensions"]'); + await extTab.click({ timeout: TIMEOUT_MS }); + + // Verify extensions panel is visible + const extPanel = page.locator('[data-page-settings-panel="extensions"]'); + await extPanel.waitFor({ state: "visible", timeout: TIMEOUT_MS }); + const extVisible = await extPanel.isVisible(); + if (!extVisible) fail("扩展面板不可见", "extensions panel hidden"); + else console.log(" ✅ 扩展面板可见"); + + // Verify 4 sub-tabs + const expectedSubTabs = ["知识库", "Skills", "MCP", "Agent 工具"]; + for (const tabName of expectedSubTabs) { + const tab = page.locator(`[data-extensions-subtab]`).filter({ hasText: tabName }); + const visible = await tab.isVisible().catch(() => false); + if (!visible) fail(`扩展子tab不可见: ${tabName}`, "sub-tab not visible"); + else console.log(` ✅ 子tab: ${tabName}`); + } + + // Click each sub-tab and verify panel shows + for (const [i, tabName] of expectedSubTabs.entries()) { + const panelKey = ["knowledge", "skills", "mcp", "tools"][i]; + const tab = page.locator(`[data-extensions-subtab="${panelKey}"]`); + await tab.click({ timeout: TIMEOUT_MS }); + await page.waitForTimeout(500); + const panel = page.locator(`[data-extensions-panel="${panelKey}"]`); + const hiddenVal = await panel.evaluate(el => el.hidden).catch(() => true); + if (hiddenVal === true) fail(`扩展子面板不可见: ${tabName}`, `${panelKey} panel hidden (el.hidden = ${hiddenVal})`); + else console.log(` ✅ 子面板切换: ${tabName} (hidden=${hiddenVal})`); + } + + // Take screenshot + await page.screenshot({ path: path.join(OUT_DIR, "scenario1-extensions.png"), fullPage: false }); + + // ========== SCENARIO 2: 知识库资料源管理 + 检索测试 ========== + console.log("=== Scenario 2: 知识库详情页 ==="); + // Close settings first + await page.keyboard.press("Escape"); + await page.waitForTimeout(500); + + // Open knowledge-rag settings + const ragTrigger = page.locator('[data-testid="mnote-knowledge-rag-settings-toggle"]'); + const ragTriggerExists = await ragTrigger.count(); + if (ragTriggerExists === 0) { + // Try via settings extensions subpanel + await settingsTrigger.click({ timeout: TIMEOUT_MS }); + await extTab.click({ timeout: TIMEOUT_MS }); + const knowledgeSubtab = page.locator('[data-extensions-subtab="knowledge"]'); + await knowledgeSubtab.click({ timeout: TIMEOUT_MS }); + await page.waitForTimeout(500); + } else { + await ragTrigger.click({ timeout: TIMEOUT_MS }); + } + + // Check if knowledge-rag popover exists + const ragPopover = page.locator('[data-testid="mnote-knowledge-rag-settings-popover"]'); + const ragPopoverExists = await ragPopover.count(); + if (ragPopoverExists > 0) { + console.log(" ✅ 知识库设置弹窗已打开"); + + // Verify 3 tabs + const expectedRagTabs = ["资料源管理", "检索测试", "检索参数"]; + for (const tabName of expectedRagTabs) { + const tab = page.locator('[data-kb-rag-tab]').filter({ hasText: tabName }); + const visible = await tab.isVisible().catch(() => false); + if (!visible) fail(`知识库tab不可见: ${tabName}`, "kb-rag tab not visible"); + else console.log(` ✅ 知识库tab: ${tabName}`); + } + + // SCENARIO 2a: 检索测试 + const searchTab = page.locator('[data-kb-rag-tab="search"]'); + await searchTab.click({ timeout: TIMEOUT_MS }); + const searchPanel = page.locator('[data-kb-rag-panel="search"]'); + await searchPanel.waitFor({ state: "visible", timeout: TIMEOUT_MS }); + console.log(" ✅ 检索测试面板可见"); + + // Type search query + const searchInput = page.locator('[data-kb-rag-search-input="true"]'); + await searchInput.fill("test query", { timeout: TIMEOUT_MS }); + const searchBtn = page.locator('[data-kb-rag-action="search"]'); + await searchBtn.click({ timeout: TIMEOUT_MS }); + + // Wait for results (loading or results) + await page.waitForTimeout(2000); + const resultsArea = page.locator('[data-kb-rag-search-results="true"]'); + const resultsText = await resultsArea.textContent().catch(() => ""); + console.log(` 📝 检索结果: ${resultsText.slice(0, 200)}`); + + // SCENARIO 2b: 检索参数 + const paramsTab = page.locator('[data-kb-rag-tab="params"]'); + await paramsTab.click({ timeout: TIMEOUT_MS }); + const paramsPanel = page.locator('[data-kb-rag-panel="params"]'); + await paramsPanel.waitFor({ state: "visible", timeout: TIMEOUT_MS }); + console.log(" ✅ 检索参数面板可见"); + + // Verify param form elements + const modeSelect = page.locator('[data-kb-rag-param="mode"]'); + const modeVisible = await modeSelect.isVisible().catch(() => false); + if (!modeVisible) fail("检索模式下拉不可见", "mode select hidden"); + else console.log(" ✅ 检索模式下拉"); + + const topKInput = page.locator('[data-kb-rag-param="top_k"]'); + const topKVisible = await topKInput.isVisible().catch(() => false); + if (!topKVisible) fail("Top-K输入不可见", "top_k input hidden"); + else console.log(" ✅ Top-K输入"); + + const thresholdRange = page.locator('[data-kb-rag-param="similarity_threshold"]'); + const thresholdVisible = await thresholdRange.isVisible().catch(() => false); + if (!thresholdVisible) fail("相似度阈值不可见", "threshold range hidden"); + else console.log(" ✅ 相似度阈值滑块"); + + // Test save params + await topKInput.fill("20", { timeout: TIMEOUT_MS }); + const saveBtn = page.locator('[data-kb-rag-action="save-params"]'); + await saveBtn.click({ timeout: TIMEOUT_MS }); + await page.waitForTimeout(500); + console.log(" ✅ 检索参数保存"); + + await page.screenshot({ path: path.join(OUT_DIR, "scenario2-knowledge-rag.png"), fullPage: false }); + } else { + console.log(" ⚠️ 知识库弹窗未找到,可能需先配置LightRAG"); + } + + // Close popovers + await page.keyboard.press("Escape"); + await page.waitForTimeout(500); + + // ========== SCENARIO 3: 仪表盘 ========== + console.log("=== Scenario 3: 仪表盘 ==="); + await settingsTrigger.click({ timeout: TIMEOUT_MS }); + await page.waitForSelector('[data-testid="wolai-page-settings-popover"]', { timeout: TIMEOUT_MS }); + + const dashTab = page.locator('[data-page-settings-tab="dashboard"]'); + await dashTab.click({ timeout: TIMEOUT_MS }); + + const dashPanel = page.locator('[data-page-settings-panel="dashboard"]'); + await dashPanel.waitFor({ state: "visible", timeout: TIMEOUT_MS }); + console.log(" ✅ 仪表盘面板可见"); + + // Verify dashboard cards + const expectedCards = ["知识库", "Skills", "MCP", "Agent Runs"]; + for (const cardTitle of expectedCards) { + const card = page.locator('.mnote-dashboard-card-title').filter({ hasText: cardTitle }); + const visible = await card.isVisible().catch(() => false); + if (!visible) fail(`仪表盘卡片不可见: ${cardTitle}`, "dashboard card not visible"); + else console.log(` ✅ 仪表盘卡片: ${cardTitle}`); + } + + // Verify refresh button + const refreshBtn = page.locator('[data-dashboard-action="refresh"]'); + const refreshVisible = await refreshBtn.isVisible().catch(() => false); + if (!refreshVisible) fail("刷新按钮不可见", "refresh button hidden"); + else console.log(" ✅ 刷新按钮"); + + await page.screenshot({ path: path.join(OUT_DIR, "scenario3-dashboard.png"), fullPage: false }); + + // ========== SCENARIO 4: 刷新后扩展tab不丢失 ========== + console.log("=== Scenario 4: 刷新后tab状态 ==="); + await page.reload({ waitUntil: "domcontentloaded", timeout: TIMEOUT_MS }); + await page.waitForSelector('[data-testid="wolai-page-settings-trigger"]', { timeout: TIMEOUT_MS }); + await settingsTrigger.click({ timeout: TIMEOUT_MS }); + await page.waitForSelector('[data-testid="wolai-page-settings-popover"]', { timeout: TIMEOUT_MS }); + + // Check extensions tab still there + const extTabAfter = page.locator('[data-page-settings-tab="extensions"]'); + const extTabVisible = await extTabAfter.isVisible().catch(() => false); + if (!extTabVisible) fail("刷新后扩展tab不可见", "extensions tab gone after refresh"); + else console.log(" ✅ 刷新后扩展tab存在"); + + const dashTabAfter = page.locator('[data-page-settings-tab="dashboard"]'); + const dashTabVisible = await dashTabAfter.isVisible().catch(() => false); + if (!dashTabVisible) fail("刷新后仪表盘tab不可见", "dashboard tab gone after refresh"); + else console.log(" ✅ 刷新后仪表盘tab存在"); + + // ========== RESULTS ========== + const result = { + ok: failures.length === 0, + failures, + baseUrl, + outDir: OUT_DIR, + workspaceId, + documentId, + summary: failures.length === 0 + ? "全部 4 个场景验证通过:扩展tab/知识库详情/仪表盘/刷新" + : `${failures.length} 个失败: ${failures.map(f => f.label).join(", ")}`, + }; + fs.writeFileSync(path.join(OUT_DIR, "result.json"), JSON.stringify(result, null, 2), "utf8"); + console.log("\n=== RESULT ==="); + console.log(JSON.stringify(result, null, 2)); + await context.close().catch(() => undefined); + } catch (error) { + fs.writeFileSync(path.join(OUT_DIR, "failure.json"), JSON.stringify({ ok: false, error: String(error && error.stack || error), stderr }, null, 2), "utf8"); + throw error; + } finally { + if (browser) await browser.close().catch(() => undefined); + server.kill("SIGTERM"); + await new Promise((resolve) => server.once("exit", resolve)); + if (process.env.MNOTE_KEEP_YUXI_ROOT !== "1") fs.rmSync(root, { recursive: true, force: true }); + } +} + +main().catch((error) => { console.error(error && error.stack ? error.stack : error); process.exit(1); });