#49 feat: reemplazar cytoscape por vis-network con semantica de certeza en graficos de provenance

This commit is contained in:
2026-08-16 01:41:27 -05:00
parent 58b42d956b
commit 2967ea4f4d
18 changed files with 732 additions and 593 deletions

View File

@@ -1,93 +0,0 @@
import cytoscape from 'cytoscape'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import CytoscapeChart from '@/components/graph/CytoscapeChart.vue'
const tapHandler = { current: null }
const cy = {
on: vi.fn((event, selector, handler) => {
if (event === 'tap') tapHandler.current = handler
}),
elements: vi.fn(() => ({ remove: vi.fn() })),
add: vi.fn(),
layout: vi.fn(() => ({ run: vi.fn() })),
destroy: vi.fn(),
}
vi.mock('cytoscape', () => ({
default: vi.fn(() => cy),
}))
const elements = [
{ data: { id: 'product:1', kind: 'product', label: 'Panela' }, position: { x: 100, y: 100 } },
{ data: { id: 'supplier:5', kind: 'supplier', label: 'La Mesa' }, position: { x: 320, y: 100 } },
{ data: { id: 'e1', source: 'product:1', target: 'supplier:5' } },
]
const styles = [{ selector: 'node', style: { 'background-color': '#eee' } }]
beforeEach(() => {
vi.clearAllMocks()
tapHandler.current = null
})
function mountChart (props = {}) {
return mount(CytoscapeChart, {
props: { elements, styles, ...props },
})
}
describe('CytoscapeChart', () => {
it('crea la instancia de cytoscape con elements, styles y layout', () => {
mountChart()
expect(cytoscape).toHaveBeenCalledTimes(1)
expect(cytoscape).toHaveBeenCalledWith(
expect.objectContaining({
elements,
style: styles,
layout: { name: 'preset' },
})
)
})
it('permite sobreescribir el layout con el prop layout', () => {
mountChart({ layout: { name: 'breadthfirst' } })
expect(cytoscape).toHaveBeenCalledWith(
expect.objectContaining({ layout: { name: 'breadthfirst' } })
)
})
it('emite select con los datos del nodo al hacer tap', () => {
const wrapper = mountChart()
tapHandler.current({ target: { data: () => elements[0].data } })
expect(wrapper.emitted('select')).toEqual([[elements[0].data]])
})
it('actualiza la instancia cuando cambian los elements', async () => {
const wrapper = mountChart()
const next = [{ data: { id: 'organization:3', kind: 'organization', label: 'Red' } }]
await wrapper.setProps({ elements: next })
expect(cy.elements).toHaveBeenCalled()
expect(cy.add).toHaveBeenCalledWith(next)
expect(cy.layout).toHaveBeenCalled()
})
it('destruye la instancia al desmontarse', () => {
const wrapper = mountChart()
wrapper.unmount()
expect(cy.destroy).toHaveBeenCalledTimes(1)
})
it('no escucha eventos mientras no exista la instancia', () => {
mountChart()
expect(cy.on).toHaveBeenCalledWith('tap', 'node', expect.any(Function))
})
})

View File

@@ -0,0 +1,121 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import VisChart from '@/components/graph/VisChart.vue'
const vis = vi.hoisted(() => {
const networkInstance = {
on: vi.fn(),
once: vi.fn(),
setData: vi.fn(),
setOptions: vi.fn(),
fit: vi.fn(),
destroy: vi.fn(),
}
const dataSetInstance = {
get: vi.fn(),
}
return {
networkInstance,
dataSetInstance,
Network: vi.fn(() => networkInstance),
DataSet: vi.fn(() => dataSetInstance),
}
})
vi.mock('vis-network/standalone', () => ({
Network: vis.Network,
DataSet: vis.DataSet,
}))
function handlerFor (mock, eventName) {
const call = mock.mock.calls.find(args => args[0] === eventName)
return call ? call[1] : null
}
describe('VisChart', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('crea la red con los nodos y aristas recibidos', () => {
const nodes = [{ id: 'product:1', label: 'Panela' }]
const edges = [{ from: 'product:1', to: 'supplier:5', dashes: true }]
mount(VisChart, { props: { nodes, edges } })
expect(vis.Network).toHaveBeenCalledTimes(1)
const [container, , options] = vis.Network.mock.calls[0]
expect(container).toBeInstanceOf(HTMLElement)
expect(options.physics.stabilization.enabled).toBe(true)
expect(vis.dataSetInstance.get).toBeDefined()
const data = vis.networkInstance.setData.mock.calls[0][0]
expect(data.nodes).toBe(vis.dataSetInstance)
expect(data.edges).toBe(vis.dataSetInstance)
})
it('mezcla las opciones base con las recibidas', () => {
mount(VisChart, {
props: {
nodes: [],
edges: [],
options: { nodes: { font: { size: 16 } }, physics: { stabilization: { iterations: 100 } } },
},
})
const options = vis.Network.mock.calls[0][2]
expect(options.nodes.font.size).toBe(16)
expect(options.nodes.shape).toBe('dot')
expect(options.physics.stabilization.iterations).toBe(100)
expect(options.physics.barnesHut.springLength).toBe(140)
})
it('emite select con el nodo al hacer click', () => {
const wrapper = mount(VisChart, { props: { nodes: [{ id: 'supplier:5', kind: 'supplier' }], edges: [] } })
vis.dataSetInstance.get.mockReturnValue({ id: 'supplier:5', kind: 'supplier', label: 'La Mesa' })
handlerFor(vis.networkInstance.on, 'click')({ nodes: ['supplier:5'] })
expect(wrapper.emitted('select')[0][0]).toEqual({ id: 'supplier:5', kind: 'supplier', label: 'La Mesa' })
})
it('no emite select cuando el click no cae sobre un nodo', () => {
const wrapper = mount(VisChart, { props: { nodes: [], edges: [] } })
handlerFor(vis.networkInstance.on, 'click')({ nodes: [] })
expect(wrapper.emitted('select')).toBeUndefined()
})
it('desactiva la física y ajusta la vista cuando la estabilización termina', () => {
mount(VisChart, { props: { nodes: [], edges: [] } })
const onStabilized = handlerFor(vis.networkInstance.once, 'stabilizationIterationsDone')
expect(onStabilized).toBeDefined()
onStabilized()
expect(vis.networkInstance.setOptions).toHaveBeenCalledWith({ physics: { enabled: false } })
expect(vis.networkInstance.fit).toHaveBeenCalled()
})
it('actualiza la red cuando cambian los nodos o aristas', async () => {
const wrapper = mount(VisChart, { props: { nodes: [{ id: 'product:1' }], edges: [] } })
expect(vis.networkInstance.setData).toHaveBeenCalledTimes(1)
await wrapper.setProps({ nodes: [{ id: 'product:2' }], edges: [{ from: 'product:2', to: 'supplier:5' }] })
expect(vis.networkInstance.setData).toHaveBeenCalledTimes(2)
})
it('aplica la altura recibida al contenedor', () => {
const wrapper = mount(VisChart, { props: { height: 340 } })
expect(wrapper.element.style.height).toBe('340px')
})
it('destruye la red al desmontar', () => {
const wrapper = mount(VisChart, { props: { nodes: [], edges: [] } })
wrapper.unmount()
expect(vis.networkInstance.destroy).toHaveBeenCalled()
})
})