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:
@@ -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());
|
||||
|
||||
@@ -124,7 +124,7 @@ function tabBtn(active: boolean): React.CSSProperties {
|
||||
export function GraphView({ onSwitchToEditor }: { onSwitchToEditor?: () => void }) {
|
||||
const [mode, setMode] = useState<'mindmap' | 'graph'>('mindmap');
|
||||
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 }}>
|
||||
<button style={tabBtn(mode === 'mindmap')} onClick={() => setMode('mindmap')}>思维导图</button>
|
||||
<button style={tabBtn(mode === 'graph')} onClick={() => setMode('graph')}>知识图谱</button>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user