From e20fcab837c677eaa21e0eb766df21e8c1fdc0aa Mon Sep 17 00:00:00 2001 From: hunter-dev Date: Thu, 27 Aug 2026 17:22:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=BE=E8=B0=B1/=E6=80=9D=E7=BB=B4?= =?UTF-8?q?=E5=AF=BC=E5=9B=BE=E9=AB=98=E5=BA=A6=E5=A1=8C=E9=99=B7=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E4=B8=8D=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真正根因: GraphView 容器有 flex:1(在行布局中)但不是 display:flex, 子组件 MindmapTree/LinkGraph 的 flex:1 被忽略 → 高度 auto = 0 (子内容全是绝对定位,无在流内容) → SVG 高度 0 → fit() 计算 NaN 持续报 translate(NaN,NaN) 错误,内容不可见 之前 E2E 只数 DOM 元素数量不检查可见性,掩盖了此 bug 修复: GraphView 容器加 display:flex + flexDirection:column, 子组件 flex:1 正常生效,容器高度正确,内容可见 附带: - 主进程转发渲染进程 console 到终端(便于 dev 调试) - graph-render E2E 测试改为检查可见性(getBoundingClientRect) - 修复 TabBar 关闭全部测试的 flaky 断言(expect.poll 自动重试) --- apps/desktop/src/main/index.ts | 5 ++ .../src/renderer/features/graph/GraphView.tsx | 2 +- apps/desktop/tests/e2e/button-all.spec.ts | 2 +- apps/desktop/tests/e2e/graph-render.spec.ts | 61 ++++++++++++------- 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 126bcf3..14c7a8c 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -26,6 +26,11 @@ function createWindow() { } }); win.on('ready-to-show', () => win?.show()); + // 转发渲染进程控制台消息到主进程终端(便于开发调试) + win.webContents.on('console-message', (e, level, message, line, sourceId) => { + const lv = ['log', 'info', 'warn', 'error'][level] || 'log'; + console.log('[renderer:' + lv + '] ' + message + (sourceId ? ' (' + sourceId + ':' + line + ')' : '')); + }); win.on('closed', () => { win = null; }); // 窗口控制(FR-W2) ipcMain.on(IPC.winMinimize, () => win?.minimize()); diff --git a/apps/desktop/src/renderer/features/graph/GraphView.tsx b/apps/desktop/src/renderer/features/graph/GraphView.tsx index 5d62c7b..92a9d31 100644 --- a/apps/desktop/src/renderer/features/graph/GraphView.tsx +++ b/apps/desktop/src/renderer/features/graph/GraphView.tsx @@ -124,7 +124,7 @@ function tabBtn(active: boolean): React.CSSProperties { export function GraphView({ onSwitchToEditor }: { onSwitchToEditor?: () => void }) { const [mode, setMode] = useState<'mindmap' | 'graph'>('mindmap'); return ( -
+
diff --git a/apps/desktop/tests/e2e/button-all.spec.ts b/apps/desktop/tests/e2e/button-all.spec.ts index 0c19676..ad82f51 100644 --- a/apps/desktop/tests/e2e/button-all.spec.ts +++ b/apps/desktop/tests/e2e/button-all.spec.ts @@ -335,7 +335,7 @@ test('TabBar: 关闭全部标签后编辑器应显示空状态', async () => { await tab.locator('span').filter({ hasText: 'x' }).click(); await a.page.waitForTimeout(500); // 标签栏为空 → 当前文档应重置,编辑器显示空状态提示 - expect(await a.page.evaluate(() => (globalThis as any).__STORE__?.getState?.().currentPath)).toBeNull(); + await expect.poll(() => a.page.evaluate(() => (globalThis as any).__STORE__?.getState?.().currentPath), { timeout: 5000 }).toBeNull(); await expect(a.page.getByText('从左侧选择一篇笔记')).toBeVisible({ timeout: 5000 }); await a.app.close(); }); diff --git a/apps/desktop/tests/e2e/graph-render.spec.ts b/apps/desktop/tests/e2e/graph-render.spec.ts index 1fca49c..790a71f 100644 --- a/apps/desktop/tests/e2e/graph-render.spec.ts +++ b/apps/desktop/tests/e2e/graph-render.spec.ts @@ -6,7 +6,7 @@ import os from 'node:os'; const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js'); -test('思维导图+知识图谱渲染验证', async () => { +test('思维导图+知识图谱渲染验证(含可见性)', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-graph-fix-')); const userData = nodePath.join(tmp, 'userdata'); const vault = nodePath.join(tmp, 'vault'); @@ -23,41 +23,56 @@ test('思维导图+知识图谱渲染验证', async () => { await page.evaluate(() => (window as any).__STORE__.getState().loadTree()); await page.waitForTimeout(1000); - // Verify IPC.graph returns correct edges - const gd: any = await invoke('graph', null); - console.log('IPC.graph nodes:', gd.nodes.length, 'edges:', JSON.stringify(gd.edges)); - // Switch to graph view (default mode is mindmap) await page.getByTitle('切换 编辑器/图谱').click(); await page.waitForTimeout(2000); - const mindmapSvg = await page.evaluate(() => { + // Mindmap: verify SVG has non-zero size AND visible content + const mindmap = await page.evaluate(() => { const svg = document.querySelector('.markmap-dark svg'); - if (!svg) return { found: false, total: 0 }; - const g = svg.querySelectorAll('g').length; + if (!svg) return { found: false }; + const rect = svg.getBoundingClientRect(); const fo = svg.querySelectorAll('foreignObject').length; - const paths = svg.querySelectorAll('path').length; - return { found: true, total: g + fo + paths }; + const g = svg.querySelectorAll('g').length; + // Check that content is actually visible (has bounding boxes within the svg) + const visibleContent = Array.from(svg.querySelectorAll('g.markmap-node')).some((el) => { + const r = el.getBoundingClientRect(); + return r.width > 0 && r.height > 0; + }); + return { found: true, w: rect.width, h: rect.height, foreignObject: fo, g, visibleContent }; }); - console.log('Mindmap SVG content:', JSON.stringify(mindmapSvg)); - expect(mindmapSvg.found).toBe(true); - expect(mindmapSvg.total).toBeGreaterThan(5); + console.log('Mindmap:', JSON.stringify(mindmap)); + expect(mindmap.found).toBe(true); + expect(mindmap.h).toBeGreaterThan(100); // svg must have real height + expect(mindmap.foreignObject).toBeGreaterThan(0); + expect(mindmap.visibleContent).toBe(true); // content must be visibly rendered // Switch to knowledge graph mode await page.getByRole('button', { name: '知识图谱' }).click(); await page.waitForTimeout(3000); - const graphContent = await page.evaluate(() => { - return { - rects: document.querySelectorAll('svg rect').length, - texts: document.querySelectorAll('svg text').length, - lines: document.querySelectorAll('svg line').length, - }; + const graph = await page.evaluate(() => { + const svgs = document.querySelectorAll('svg'); + let svg: SVGElement | null = null; + for (const s of svgs) { if (s.querySelectorAll('g[transform]').length > 0 || s.querySelectorAll('rect').length > 1) { svg = s; break; } } + if (!svg) return { found: false }; + const rect = svg.getBoundingClientRect(); + const rects = svg.querySelectorAll('rect').length; + const texts = svg.querySelectorAll('text').length; + const lines = svg.querySelectorAll('line').length; + // Check a node card is visible within the svg + const visibleCard = Array.from(svg.querySelectorAll('rect')).some((r) => { + const b = r.getBoundingClientRect(); + return b.width > 0 && b.height > 0; + }); + return { found: true, w: rect.width, h: rect.height, rects, texts, lines, visibleCard }; }); - console.log('Knowledge graph SVG content:', JSON.stringify(graphContent)); - expect(graphContent.rects).toBeGreaterThanOrEqual(5); - expect(graphContent.texts).toBeGreaterThanOrEqual(3); - expect(graphContent.lines).toBeGreaterThanOrEqual(1); // doc1 -> doc2 edge + console.log('Knowledge graph:', JSON.stringify(graph)); + expect(graph.found).toBe(true); + expect(graph.h).toBeGreaterThan(100); + expect(graph.rects).toBeGreaterThanOrEqual(5); + expect(graph.lines).toBeGreaterThanOrEqual(1); + expect(graph.visibleCard).toBe(true); await app.close(); }); \ No newline at end of file