fix: 图谱/思维导图高度塌陷导致不显示

真正根因: 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 自动重试)
This commit is contained in:
hunter-dev
2026-08-27 17:22:49 +08:00
parent 96f5c72fa7
commit e20fcab837
4 changed files with 45 additions and 25 deletions
+5
View File
@@ -26,6 +26,11 @@ function createWindow() {
} }
}); });
win.on('ready-to-show', () => win?.show()); 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; }); win.on('closed', () => { win = null; });
// 窗口控制(FR-W2) // 窗口控制(FR-W2)
ipcMain.on(IPC.winMinimize, () => win?.minimize()); ipcMain.on(IPC.winMinimize, () => win?.minimize());
@@ -124,7 +124,7 @@ function tabBtn(active: boolean): React.CSSProperties {
export function GraphView({ onSwitchToEditor }: { onSwitchToEditor?: () => void }) { export function GraphView({ onSwitchToEditor }: { onSwitchToEditor?: () => void }) {
const [mode, setMode] = useState<'mindmap' | 'graph'>('mindmap'); const [mode, setMode] = useState<'mindmap' | 'graph'>('mindmap');
return ( return (
<div style={{ flex: 1, position: 'relative', overflow: 'hidden', background: 'var(--bg-secondary)' }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', position: 'relative', overflow: 'hidden', background: 'var(--bg-secondary)' }}>
<div style={{ position: 'absolute', top: 8, left: 8, zIndex: 10, display: 'flex', gap: 4 }}> <div style={{ position: 'absolute', top: 8, left: 8, zIndex: 10, display: 'flex', gap: 4 }}>
<button style={tabBtn(mode === 'mindmap')} onClick={() => setMode('mindmap')}>思维导图</button> <button style={tabBtn(mode === 'mindmap')} onClick={() => setMode('mindmap')}>思维导图</button>
<button style={tabBtn(mode === 'graph')} onClick={() => setMode('graph')}>知识图谱</button> <button style={tabBtn(mode === 'graph')} onClick={() => setMode('graph')}>知识图谱</button>
+1 -1
View File
@@ -335,7 +335,7 @@ test('TabBar: 关闭全部标签后编辑器应显示空状态', async () => {
await tab.locator('span').filter({ hasText: 'x' }).click(); await tab.locator('span').filter({ hasText: 'x' }).click();
await a.page.waitForTimeout(500); 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 expect(a.page.getByText('从左侧选择一篇笔记')).toBeVisible({ timeout: 5000 });
await a.app.close(); await a.app.close();
}); });
+38 -23
View File
@@ -6,7 +6,7 @@ import os from 'node:os';
const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js'); 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 tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-graph-fix-'));
const userData = nodePath.join(tmp, 'userdata'); const userData = nodePath.join(tmp, 'userdata');
const vault = nodePath.join(tmp, 'vault'); const vault = nodePath.join(tmp, 'vault');
@@ -23,41 +23,56 @@ test('思维导图+知识图谱渲染验证', async () => {
await page.evaluate(() => (window as any).__STORE__.getState().loadTree()); await page.evaluate(() => (window as any).__STORE__.getState().loadTree());
await page.waitForTimeout(1000); 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) // Switch to graph view (default mode is mindmap)
await page.getByTitle('切换 编辑器/图谱').click(); await page.getByTitle('切换 编辑器/图谱').click();
await page.waitForTimeout(2000); 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'); const svg = document.querySelector('.markmap-dark svg');
if (!svg) return { found: false, total: 0 }; if (!svg) return { found: false };
const g = svg.querySelectorAll('g').length; const rect = svg.getBoundingClientRect();
const fo = svg.querySelectorAll('foreignObject').length; const fo = svg.querySelectorAll('foreignObject').length;
const paths = svg.querySelectorAll('path').length; const g = svg.querySelectorAll('g').length;
return { found: true, total: g + fo + paths }; // 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)); console.log('Mindmap:', JSON.stringify(mindmap));
expect(mindmapSvg.found).toBe(true); expect(mindmap.found).toBe(true);
expect(mindmapSvg.total).toBeGreaterThan(5); 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 // Switch to knowledge graph mode
await page.getByRole('button', { name: '知识图谱' }).click(); await page.getByRole('button', { name: '知识图谱' }).click();
await page.waitForTimeout(3000); await page.waitForTimeout(3000);
const graphContent = await page.evaluate(() => { const graph = await page.evaluate(() => {
return { const svgs = document.querySelectorAll('svg');
rects: document.querySelectorAll('svg rect').length, let svg: SVGElement | null = null;
texts: document.querySelectorAll('svg text').length, for (const s of svgs) { if (s.querySelectorAll('g[transform]').length > 0 || s.querySelectorAll('rect').length > 1) { svg = s; break; } }
lines: document.querySelectorAll('svg line').length, 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)); console.log('Knowledge graph:', JSON.stringify(graph));
expect(graphContent.rects).toBeGreaterThanOrEqual(5); expect(graph.found).toBe(true);
expect(graphContent.texts).toBeGreaterThanOrEqual(3); expect(graph.h).toBeGreaterThan(100);
expect(graphContent.lines).toBeGreaterThanOrEqual(1); // doc1 -> doc2 edge expect(graph.rects).toBeGreaterThanOrEqual(5);
expect(graph.lines).toBeGreaterThanOrEqual(1);
expect(graph.visibleCard).toBe(true);
await app.close(); await app.close();
}); });