#49 feat: reemplazar cytoscape por vis-network con semantica de certeza en graficos de provenance
This commit is contained in:
@@ -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))
|
||||
})
|
||||
})
|
||||
121
tests/unit/components/graph/VisChart.spec.js
Normal file
121
tests/unit/components/graph/VisChart.spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -1,20 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CytoscapeChart from '@/components/graph/CytoscapeChart.vue'
|
||||
import ProvenanceDetailModal from '@/components/provenance/ProvenanceDetailModal.vue'
|
||||
import ProductSupplierOrganizationChart from '@/components/provenance/ProductSupplierOrganizationChart.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
const cy = {
|
||||
on: vi.fn(),
|
||||
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),
|
||||
vi.mock('@/components/graph/VisChart.vue', () => ({
|
||||
default: {
|
||||
name: 'VisChart',
|
||||
template: '<div class="vis-chart-stub" />',
|
||||
props: ['nodes', 'edges', 'height', 'options'],
|
||||
},
|
||||
}))
|
||||
|
||||
const provenance = [
|
||||
@@ -36,6 +31,22 @@ const provenance = [
|
||||
},
|
||||
]
|
||||
|
||||
const provenanceWithTwoSuppliers = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela regional por Kg' },
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa' },
|
||||
organization: { id: 3, name: 'Red de Economía Solidaria' },
|
||||
},
|
||||
{
|
||||
supplier: { id: 6, name: 'Asociación San Antonio' },
|
||||
organization: { id: 3, name: 'Red de Economía Solidaria' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function mountChart (props = {}) {
|
||||
return mount(ProductSupplierOrganizationChart, {
|
||||
props: { provenance, ...props },
|
||||
@@ -43,29 +54,58 @@ function mountChart (props = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
function visChart (wrapper) {
|
||||
return wrapper.findComponent({ name: 'VisChart' })
|
||||
}
|
||||
|
||||
describe('ProductSupplierOrganizationChart', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renderiza el gráfico con los elementos de productos, proveedores y organizaciones', () => {
|
||||
it('renderiza el gráfico con nodos de productos, proveedores y organizaciones', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const chart = wrapper.findComponent(CytoscapeChart)
|
||||
const chart = visChart(wrapper)
|
||||
expect(chart.exists()).toBe(true)
|
||||
const elements = chart.props('elements')
|
||||
const nodeIds = elements.filter(e => e.data.source === undefined).map(e => e.data.id)
|
||||
const nodeIds = chart.props('nodes').map(node => node.id)
|
||||
expect(nodeIds).toContain('product:1')
|
||||
expect(nodeIds).toContain('supplier:5')
|
||||
expect(nodeIds).toContain('organization:3')
|
||||
})
|
||||
|
||||
it('muestra el nombre de los nodos a través del label', () => {
|
||||
it('incluye la etiqueta del nodo y la imagen circular para quien tiene foto', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const styles = wrapper.findComponent(CytoscapeChart).props('styles')
|
||||
const nodeStyle = styles.find(style => style.selector === 'node').style
|
||||
expect(nodeStyle.content).toBe('data(label)')
|
||||
const nodes = visChart(wrapper).props('nodes')
|
||||
const product = nodes.find(node => node.id === 'product:1')
|
||||
expect(product.label).toBe('Panela regional por Kg')
|
||||
expect(product.image).toBe('http://localhost/media/panela.jpg')
|
||||
expect(product.shape).toBe('circularImage')
|
||||
})
|
||||
|
||||
it('con varios proveedores crea el punto de disyunción: sólida hasta él y discontinua hacia cada proveedor', () => {
|
||||
const wrapper = mountChart({ provenance: provenanceWithTwoSuppliers })
|
||||
|
||||
const chart = visChart(wrapper)
|
||||
const nodes = chart.props('nodes')
|
||||
const junction = nodes.find(node => node.id === 'junction:1')
|
||||
expect(junction).toBeDefined()
|
||||
expect(junction.shape).toBe('dot')
|
||||
expect(junction.label).toBe('')
|
||||
|
||||
const edges = chart.props('edges')
|
||||
expect(edges.find(edge => edge.from === 'product:1' && edge.to === 'junction:1').dashes).toBe(false)
|
||||
expect(edges.find(edge => edge.from === 'junction:1' && edge.to === 'supplier:5').dashes).toBe(true)
|
||||
expect(edges.find(edge => edge.from === 'junction:1' && edge.to === 'supplier:6').dashes).toBe(true)
|
||||
})
|
||||
|
||||
it('una organización compartida viaja en línea continua', () => {
|
||||
const wrapper = mountChart({ provenance: provenanceWithTwoSuppliers })
|
||||
|
||||
const edges = visChart(wrapper).props('edges')
|
||||
expect(edges.find(edge => edge.from === 'supplier:5' && edge.to === 'organization:3').dashes).toBe(false)
|
||||
expect(edges.find(edge => edge.from === 'supplier:6' && edge.to === 'organization:3').dashes).toBe(false)
|
||||
})
|
||||
|
||||
it('no muestra mensaje de próximamente cuando existen relaciones', () => {
|
||||
@@ -79,17 +119,24 @@ describe('ProductSupplierOrganizationChart', () => {
|
||||
provenance: [{ product: { id: 1, name: 'Panela' }, suppliers: [] }],
|
||||
})
|
||||
|
||||
expect(wrapper.findComponent(CytoscapeChart).exists()).toBe(false)
|
||||
expect(visChart(wrapper).exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('próximamente')
|
||||
})
|
||||
|
||||
it('abre el modal de detalle al seleccionar un nodo', async () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const chart = wrapper.findComponent(CytoscapeChart)
|
||||
await chart.vm.$emit('select', { kind: 'supplier', label: 'Asociación Agropecuaria La Mesa', entity: { kind: 'supplier' } })
|
||||
await visChart(wrapper).vm.$emit('select', { id: 'supplier:5', kind: 'supplier', label: 'Asociación Agropecuaria La Mesa', entity: { kind: 'supplier' } })
|
||||
|
||||
expect(wrapper.findComponent(ProvenanceDetailModal).props('visible')).toBe(true)
|
||||
expect(document.body.textContent).toContain('Asociación Agropecuaria La Mesa')
|
||||
})
|
||||
|
||||
it('ignora la selección del punto de disyunción', async () => {
|
||||
const wrapper = mountChart({ provenance: provenanceWithTwoSuppliers })
|
||||
|
||||
await visChart(wrapper).vm.$emit('select', { id: 'junction:1', kind: 'junction', label: '' })
|
||||
|
||||
expect(wrapper.findComponent(ProvenanceDetailModal).props('visible')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CytoscapeChart from '@/components/graph/CytoscapeChart.vue'
|
||||
import ProvenanceDetailModal from '@/components/provenance/ProvenanceDetailModal.vue'
|
||||
import ProductTerritoryChart from '@/components/provenance/ProductTerritoryChart.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
const cy = {
|
||||
on: vi.fn(),
|
||||
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),
|
||||
vi.mock('@/components/graph/VisChart.vue', () => ({
|
||||
default: {
|
||||
name: 'VisChart',
|
||||
template: '<div class="vis-chart-stub" />',
|
||||
props: ['nodes', 'edges', 'height', 'options'],
|
||||
},
|
||||
}))
|
||||
|
||||
const provenance = [
|
||||
@@ -32,6 +27,26 @@ const provenance = [
|
||||
},
|
||||
]
|
||||
|
||||
const provenanceWithTwoSuppliers = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela regional por Kg' },
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa' },
|
||||
municipality: { id: 7, name: 'La Mesa' },
|
||||
department: { id: 2, name: 'Cundinamarca' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
},
|
||||
{
|
||||
supplier: { id: 6, name: 'Asociación San Antonio' },
|
||||
municipality: { id: 8, name: 'San Antonio' },
|
||||
department: { id: 3, name: 'Antioquia' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function mountChart (props = {}) {
|
||||
return mount(ProductTerritoryChart, {
|
||||
props: { provenance, ...props },
|
||||
@@ -39,6 +54,10 @@ function mountChart (props = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
function visChart (wrapper) {
|
||||
return wrapper.findComponent({ name: 'VisChart' })
|
||||
}
|
||||
|
||||
describe('ProductTerritoryChart', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -47,10 +66,9 @@ describe('ProductTerritoryChart', () => {
|
||||
it('renderiza el gráfico con municipio, departamento y país', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const chart = wrapper.findComponent(CytoscapeChart)
|
||||
const chart = visChart(wrapper)
|
||||
expect(chart.exists()).toBe(true)
|
||||
const elements = chart.props('elements')
|
||||
const nodeIds = elements.filter(e => e.data.source === undefined).map(e => e.data.id)
|
||||
const nodeIds = chart.props('nodes').map(node => node.id)
|
||||
expect(nodeIds).toContain('product:1')
|
||||
expect(nodeIds).toContain('supplier:5')
|
||||
expect(nodeIds).toContain('municipality:7')
|
||||
@@ -58,12 +76,27 @@ describe('ProductTerritoryChart', () => {
|
||||
expect(nodeIds).toContain('country:1')
|
||||
})
|
||||
|
||||
it('muestra el nombre de los nodos a través del label', () => {
|
||||
it('incluye la etiqueta del nodo y el color según el tipo', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const styles = wrapper.findComponent(CytoscapeChart).props('styles')
|
||||
const nodeStyle = styles.find(style => style.selector === 'node').style
|
||||
expect(nodeStyle.content).toBe('data(label)')
|
||||
const nodes = visChart(wrapper).props('nodes')
|
||||
const municipality = nodes.find(node => node.id === 'municipality:7')
|
||||
expect(municipality.label).toBe('La Mesa')
|
||||
expect(municipality.color).toBe('#66bb6a')
|
||||
})
|
||||
|
||||
it('con varios proveedores la duda se corta cuando comparten territorio', () => {
|
||||
const wrapper = mountChart({ provenance: provenanceWithTwoSuppliers })
|
||||
|
||||
const chart = visChart(wrapper)
|
||||
expect(chart.props('nodes').map(node => node.id)).toContain('junction:1')
|
||||
|
||||
const edges = chart.props('edges')
|
||||
expect(edges.find(edge => edge.from === 'product:1' && edge.to === 'junction:1').dashes).toBe(false)
|
||||
expect(edges.find(edge => edge.from === 'junction:1' && edge.to === 'supplier:5').dashes).toBe(true)
|
||||
expect(edges.find(edge => edge.from === 'supplier:5' && edge.to === 'municipality:7').dashes).toBe(true)
|
||||
expect(edges.find(edge => edge.from === 'municipality:7' && edge.to === 'department:2').dashes).toBe(true)
|
||||
expect(edges.find(edge => edge.from === 'department:2' && edge.to === 'country:1').dashes).toBe(false)
|
||||
})
|
||||
|
||||
it('muestra mensaje de próximamente cuando ningún proveedor tiene territorio', () => {
|
||||
@@ -76,15 +109,14 @@ describe('ProductTerritoryChart', () => {
|
||||
],
|
||||
})
|
||||
|
||||
expect(wrapper.findComponent(CytoscapeChart).exists()).toBe(false)
|
||||
expect(visChart(wrapper).exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('próximamente')
|
||||
})
|
||||
|
||||
it('abre el modal de detalle al seleccionar un municipio', async () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const chart = wrapper.findComponent(CytoscapeChart)
|
||||
await chart.vm.$emit('select', { kind: 'municipality', label: 'La Mesa', entity: { kind: 'municipality', name: 'La Mesa' } })
|
||||
await visChart(wrapper).vm.$emit('select', { id: 'municipality:7', kind: 'municipality', label: 'La Mesa', entity: { kind: 'municipality', name: 'La Mesa' } })
|
||||
|
||||
expect(wrapper.findComponent(ProvenanceDetailModal).props('visible')).toBe(true)
|
||||
expect(document.body.textContent).toContain('La Mesa')
|
||||
|
||||
@@ -5,16 +5,12 @@ import ProductTerritoryChart from '@/components/provenance/ProductTerritoryChart
|
||||
import ProvenanceSection from '@/components/provenance/ProvenanceSection.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
const cy = {
|
||||
on: vi.fn(),
|
||||
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),
|
||||
vi.mock('@/components/graph/VisChart.vue', () => ({
|
||||
default: {
|
||||
name: 'VisChart',
|
||||
template: '<div class="vis-chart-stub" />',
|
||||
props: ['nodes', 'edges', 'height', 'options'],
|
||||
},
|
||||
}))
|
||||
|
||||
const provenance = [
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
hasAnyTerritory,
|
||||
} from '@/components/provenance/provenance-graph'
|
||||
|
||||
const provenance = [
|
||||
const singleSupplier = [
|
||||
{
|
||||
product: {
|
||||
id: 1,
|
||||
@@ -25,9 +25,37 @@ const provenance = [
|
||||
},
|
||||
]
|
||||
|
||||
const twoSuppliers = (overrides = {}) => [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación A' },
|
||||
organization: { id: 3, name: 'Red' },
|
||||
municipality: { id: 7, name: 'La Mesa' },
|
||||
department: { id: 2, name: 'Cundinamarca' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
...(overrides.first || {}),
|
||||
},
|
||||
{
|
||||
supplier: { id: 6, name: 'Asociación B' },
|
||||
organization: { id: 3, name: 'Red' },
|
||||
municipality: { id: 8, name: 'San Antonio' },
|
||||
department: { id: 3, name: 'Antioquia' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
...(overrides.second || {}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function edgeOf (graph, from, to) {
|
||||
return graph.edges.find(edge => edge.from === from && edge.to === to)
|
||||
}
|
||||
|
||||
describe('hasAnySupplier', () => {
|
||||
it('es true cuando algún producto tiene proveedores', () => {
|
||||
expect(hasAnySupplier(provenance)).toBe(true)
|
||||
expect(hasAnySupplier(singleSupplier)).toBe(true)
|
||||
})
|
||||
|
||||
it('es false cuando todos los productos están sin proveedores', () => {
|
||||
@@ -42,7 +70,7 @@ describe('hasAnySupplier', () => {
|
||||
|
||||
describe('hasAnyTerritory', () => {
|
||||
it('es true cuando algún proveedor tiene territorio', () => {
|
||||
expect(hasAnyTerritory(provenance)).toBe(true)
|
||||
expect(hasAnyTerritory(singleSupplier)).toBe(true)
|
||||
})
|
||||
|
||||
it('es false cuando todos los territorios vienen en null', () => {
|
||||
@@ -62,20 +90,41 @@ describe('hasAnyTerritory', () => {
|
||||
})
|
||||
|
||||
describe('buildSupplierOrganizationGraph', () => {
|
||||
it('crea nodos de producto, proveedor y organización con sus aristas', () => {
|
||||
const graph = buildSupplierOrganizationGraph(provenance)
|
||||
it('con un único proveedor conecta con arista cierta y sin junction', () => {
|
||||
const graph = buildSupplierOrganizationGraph(singleSupplier)
|
||||
|
||||
const ids = graph.nodes.map(n => n.id)
|
||||
expect(ids).toContain('product:1')
|
||||
expect(ids).toContain('supplier:5')
|
||||
expect(ids).toContain('organization:3')
|
||||
expect(graph.nodes.map(n => n.id)).not.toContain('junction:1')
|
||||
expect(edgeOf(graph, 'product:1', 'supplier:5')).toEqual({ from: 'product:1', to: 'supplier:5', certain: true })
|
||||
})
|
||||
|
||||
expect(graph.edges).toContainEqual({ from: 'product:1', to: 'supplier:5' })
|
||||
expect(graph.edges).toContainEqual({ from: 'supplier:5', to: 'organization:3' })
|
||||
it('con varios proveedores crea un punto de disyunción: sólida hasta él y dudosa hacia cada proveedor', () => {
|
||||
const graph = buildSupplierOrganizationGraph(twoSuppliers())
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).toContain('junction:1')
|
||||
expect(edgeOf(graph, 'product:1', 'junction:1')).toEqual({ from: 'product:1', to: 'junction:1', certain: true })
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:5')).toEqual({ from: 'junction:1', to: 'supplier:5', certain: false })
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:6')).toEqual({ from: 'junction:1', to: 'supplier:6', certain: false })
|
||||
})
|
||||
|
||||
it('una organización compartida por todos los proveedores es cierta', () => {
|
||||
const graph = buildSupplierOrganizationGraph(twoSuppliers())
|
||||
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3')).toEqual({ from: 'supplier:5', to: 'organization:3', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:6', 'organization:3')).toEqual({ from: 'supplier:6', to: 'organization:3', certain: true })
|
||||
})
|
||||
|
||||
it('organizaciones distintas entre proveedores son dudosas', () => {
|
||||
const graph = buildSupplierOrganizationGraph(twoSuppliers({
|
||||
first: { organization: { id: 3, name: 'Red' } },
|
||||
second: { organization: { id: 4, name: 'Cooperativa' } },
|
||||
}))
|
||||
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:6', 'organization:4').certain).toBe(false)
|
||||
})
|
||||
|
||||
it('incluye la imagen del producto y el detalle de la entidad', () => {
|
||||
const graph = buildSupplierOrganizationGraph(provenance)
|
||||
const graph = buildSupplierOrganizationGraph(singleSupplier)
|
||||
const product = graph.nodes.find(n => n.id === 'product:1')
|
||||
|
||||
expect(product.image).toBe('http://localhost/media/panela.jpg')
|
||||
@@ -94,7 +143,7 @@ describe('buildSupplierOrganizationGraph', () => {
|
||||
const graph = buildSupplierOrganizationGraph(data)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).not.toContain('organization:')
|
||||
expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5' }])
|
||||
expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5', certain: true }])
|
||||
})
|
||||
|
||||
it('no duplica nodos que se repiten entre productos', () => {
|
||||
@@ -109,15 +158,12 @@ describe('buildSupplierOrganizationGraph', () => {
|
||||
expect(graph.edges).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('ubica producto, proveedor y organización en columnas crecientes', () => {
|
||||
const graph = buildSupplierOrganizationGraph(provenance)
|
||||
|
||||
it('no incluye posiciones de layout', () => {
|
||||
const graph = buildSupplierOrganizationGraph(singleSupplier)
|
||||
const product = graph.nodes.find(n => n.id === 'product:1')
|
||||
const supplier = graph.nodes.find(n => n.id === 'supplier:5')
|
||||
const organization = graph.nodes.find(n => n.id === 'organization:3')
|
||||
|
||||
expect(product.x).toBeLessThan(supplier.x)
|
||||
expect(supplier.x).toBeLessThan(organization.x)
|
||||
expect(product.x).toBeUndefined()
|
||||
expect(product.y).toBeUndefined()
|
||||
})
|
||||
|
||||
it('devuelve un grafo vacío cuando no hay provenance', () => {
|
||||
@@ -129,19 +175,49 @@ describe('buildSupplierOrganizationGraph', () => {
|
||||
})
|
||||
|
||||
describe('buildTerritoryGraph', () => {
|
||||
it('crea la cadena producto → proveedor → municipio → departamento → país', () => {
|
||||
const graph = buildTerritoryGraph(provenance)
|
||||
it('con un único proveedor toda la cadena es cierta', () => {
|
||||
const graph = buildTerritoryGraph(singleSupplier)
|
||||
|
||||
const ids = graph.nodes.map(n => n.id)
|
||||
expect(ids).toContain('product:1')
|
||||
expect(ids).toContain('supplier:5')
|
||||
expect(ids).toContain('municipality:7')
|
||||
expect(ids).toContain('department:2')
|
||||
expect(ids).toContain('country:1')
|
||||
expect(edgeOf(graph, 'product:1', 'supplier:5')).toEqual({ from: 'product:1', to: 'supplier:5', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toEqual({ from: 'supplier:5', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2')).toEqual({ from: 'municipality:7', to: 'department:2', certain: true })
|
||||
expect(edgeOf(graph, 'department:2', 'country:1')).toEqual({ from: 'department:2', to: 'country:1', certain: true })
|
||||
})
|
||||
|
||||
expect(graph.edges).toContainEqual({ from: 'supplier:5', to: 'municipality:7' })
|
||||
expect(graph.edges).toContainEqual({ from: 'municipality:7', to: 'department:2' })
|
||||
expect(graph.edges).toContainEqual({ from: 'department:2', to: 'country:1' })
|
||||
it('cuando los proveedores comparten municipio, el territorio es cierto desde allí', () => {
|
||||
const graph = buildTerritoryGraph(twoSuppliers({
|
||||
second: { municipality: { id: 7, name: 'La Mesa' }, department: { id: 2, name: 'Cundinamarca' } },
|
||||
}))
|
||||
|
||||
expect(edgeOf(graph, 'product:1', 'junction:1').certain).toBe(true)
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:5').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toEqual({ from: 'supplier:5', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:6', 'municipality:7')).toEqual({ from: 'supplier:6', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(true)
|
||||
expect(edgeOf(graph, 'department:2', 'country:1').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('municipios distintos pero mismo departamento: dudoso hasta el municipio y cierto desde el departamento', () => {
|
||||
const graph = buildTerritoryGraph(twoSuppliers({
|
||||
second: { department: { id: 2, name: 'Cundinamarca' } },
|
||||
}))
|
||||
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:6', 'municipality:8').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2')).toEqual({ from: 'municipality:7', to: 'department:2', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:8', 'department:2')).toEqual({ from: 'municipality:8', to: 'department:2', certain: true })
|
||||
expect(edgeOf(graph, 'department:2', 'country:1').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('municipios y departamentos distintos mantienen la duda, y el país común es cierto', () => {
|
||||
const graph = buildTerritoryGraph(twoSuppliers())
|
||||
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:6', 'municipality:8').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'municipality:8', 'department:3').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'department:2', 'country:1')).toEqual({ from: 'department:2', to: 'country:1', certain: true })
|
||||
expect(edgeOf(graph, 'department:3', 'country:1')).toEqual({ from: 'department:3', to: 'country:1', certain: true })
|
||||
})
|
||||
|
||||
it('no crea nodos de territorio cuando el proveedor no tiene municipio', () => {
|
||||
@@ -154,6 +230,6 @@ describe('buildTerritoryGraph', () => {
|
||||
const graph = buildTerritoryGraph(data)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).toEqual(['product:1', 'supplier:5'])
|
||||
expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5' }])
|
||||
expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5', certain: true }])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user