- BFF 薄适配层(FastAPI,8090):QA 代理/SSE 中继/Neo4j 直连/抽取代理/会话持久化 - Vue3 前端:五相位检索流水线、文献来源与证据链、知识图谱、提取任务四 tab - 四档响应式(桌面/笔记本/平板/手机)、深浅主题 - 81 个 FR 全部实现,测试追溯矩阵 81/81,后端 121 测/前端 156 测 - 真实凭据已脱敏,运行时配置在 backend/.env(不入 git) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
3.1 KiB
JavaScript
87 lines
3.1 KiB
JavaScript
// graphLayout.test.js —— B3 单测:手写力导向布局(graphLayout.js)
|
||
import { describe, it, expect } from 'vitest';
|
||
import { forceLayout, buildPalette, hashString } from '../../../../frontend/src/components/bench/graphLayout.js';
|
||
|
||
describe('forceLayout 手写力导向(FR-KG-001)', () => {
|
||
const small = {
|
||
nodes: [
|
||
{ id: 'a', label: 'A', type: 'gene' },
|
||
{ id: 'b', label: 'B', type: 'tissue' },
|
||
{ id: 'c', label: 'C', type: 'process' },
|
||
],
|
||
edges: [
|
||
{ source: 'a', target: 'b', relation: 'expressed_in' },
|
||
{ source: 'a', target: 'c', relation: 'involved_in' },
|
||
],
|
||
};
|
||
|
||
it('输出节点带有限位置(无 NaN/Infinity),且包围盒适配画布', () => {
|
||
const { nodes } = forceLayout(small, { width: 800, height: 600 });
|
||
expect(nodes).toHaveLength(3);
|
||
for (const n of nodes) {
|
||
expect(Number.isFinite(n.x)).toBe(true);
|
||
expect(Number.isFinite(n.y)).toBe(true);
|
||
expect(n.x).toBeGreaterThanOrEqual(0);
|
||
expect(n.x).toBeLessThanOrEqual(800);
|
||
expect(n.y).toBeGreaterThanOrEqual(0);
|
||
expect(n.y).toBeLessThanOrEqual(600);
|
||
}
|
||
});
|
||
|
||
it('边端点引用不存在的节点 → 被过滤,不抛错', () => {
|
||
const { nodes, edges } = forceLayout({
|
||
nodes: [{ id: 'a' }, { id: 'b' }],
|
||
edges: [{ source: 'a', target: 'ghost' }, { source: 'a', target: 'b' }],
|
||
});
|
||
expect(nodes).toHaveLength(2);
|
||
expect(edges).toHaveLength(1);
|
||
});
|
||
|
||
it('空图 → 空结果', () => {
|
||
const { nodes } = forceLayout({ nodes: [], edges: [] });
|
||
expect(nodes).toHaveLength(0);
|
||
});
|
||
|
||
it('单节点不崩溃', () => {
|
||
const { nodes } = forceLayout({ nodes: [{ id: 'x', type: 'gene' }], edges: [] });
|
||
expect(nodes).toHaveLength(1);
|
||
expect(Number.isFinite(nodes[0].x)).toBe(true);
|
||
});
|
||
|
||
it('大图(60 节点)也能稳定布局(供降级前渲染兜底)', () => {
|
||
const nodes = [];
|
||
const edges = [];
|
||
for (let i = 0; i < 60; i += 1) {
|
||
nodes.push({ id: `n${i}`, type: `t${i % 5}` });
|
||
if (i > 0) edges.push({ source: `n${i}`, target: `n${i - 1}` });
|
||
}
|
||
const out = forceLayout({ nodes, edges }, { width: 800, height: 600, iterations: 120 });
|
||
expect(out.nodes).toHaveLength(60);
|
||
for (const n of out.nodes) {
|
||
expect(Number.isFinite(n.x)).toBe(true);
|
||
expect(Number.isFinite(n.y)).toBe(true);
|
||
}
|
||
});
|
||
|
||
it('相同输入两次运行结果一致(确定性布局)', () => {
|
||
const a = forceLayout(small, { width: 800, height: 600 });
|
||
const b = forceLayout(small, { width: 800, height: 600 });
|
||
for (let i = 0; i < a.nodes.length; i += 1) {
|
||
expect(a.nodes[i].x).toBeCloseTo(b.nodes[i].x, 6);
|
||
expect(a.nodes[i].y).toBeCloseTo(b.nodes[i].y, 6);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('buildPalette 稳定映射', () => {
|
||
it('hashString 稳定', () => {
|
||
expect(hashString('gene')).toBe(hashString('gene'));
|
||
expect(hashString('gene')).not.toBe(hashString('tissue'));
|
||
});
|
||
|
||
it('调色板大小写敏感(Gene 与 gene 视为不同 type,与后端一致)', () => {
|
||
const p = buildPalette(['Gene', 'gene']);
|
||
expect(p.get('Gene')).not.toBe(p.get('gene'));
|
||
});
|
||
});
|