#49 feat: grafico unificado de provenance con filtro por niveles y re-estabilizacion
This commit is contained in:
@@ -177,7 +177,7 @@ No hay un estilo mayoritario. El código histórico está partido:
|
||||
- Payload: `[{ product: {id, name, catalogue_images[]}, suppliers: [{ supplier: {...}, organization|null, municipality|null, department|null, country|null }] }]`.
|
||||
- **Genérico reutilizable:**
|
||||
- `src/components/graph/VisChart.vue`: wrapper de vis-network (props `nodes`, `edges`, `height`, `options`; import `import { DataSet, Network } from 'vis-network/standalone'`; emite `select` con el nodo). **OJO**: nodo con `image: null` → TypeError de vis; omitir la clave `image` si no hay foto
|
||||
- **Específico público** (`src/components/provenance/`): builders puros en `provenance-graph.js` (`buildSupplierOrganizationGraph`, `buildTerritoryGraph`, `hasAnySupplier`, `hasAnyTerritory`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:<productId>` (disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). El gráfico de territorio va solo hasta departamento (sin país, por ahora Colombia). Las aristas se deduplican por par `(from, to)` y si un mismo par repite con distinta certeza gana la duda. Los charts (que muestran "próximamente estará disponible" cuando no hay relaciones), `ProvenanceDetailModal.vue` y `ProvenanceSection.vue`
|
||||
- **Específico público** (`src/components/provenance/`): builder puro en `provenance-graph.js` (`buildProvenanceGraph(provenance, kinds)`, `hasAnySupplier`) y el adaptador vis `provenance-vis.js` (`toVisNodes`, `toVisEdges`, `chartOptions`). **Semántica de certeza**: arista con `certain: true` es continua (inequívoca) y `certain: false` es discontinua (dudosa); con varios proveedores por producto se inserta un nodo `junction:<productId>` (disyunción) con arista sólida hasta él y discontinua hacia cada proveedor; la duda se corta donde los proveedores coinciden (misma organización/municipio/departamento). Las aristas se deduplican por par `(from, to)` y si un mismo par repite con distinta certeza gana la duda. `buildProvenanceGraph` acepta los niveles a graficar (`product`, `supplier`, `organization`, `municipality`, `department`, `country`); los niveles omitidos se saltan conectando el nivel previo con el siguiente. `ProvenanceGraph.vue` unifica los charts en uno con checkboxes de filtro (por defecto solo productos y proveedores) y columnas por nivel (x fijo por tipo; la física ordena la y); muestra "próximamente estará disponible" cuando no hay relaciones. `ProvenanceDetailModal.vue` y `ProvenanceSection.vue`
|
||||
- **Admin CRUD** (`src/components/provenance/admin/`): `OrganizationsManagement.vue`, `SuppliersManagement.vue`, `GeographyManagement.vue` (tabs países/departamentos/municipios), `SupplierLinkDialog.vue` (vincula productos↔proveedores, abierto desde `ProductsManagement.vue`). Páginas en `src/pages/admin/{organizations,suppliers,geography}.vue`; rutas en `ADMIN_ROUTES` (`router/index.js`); ítems en `NavBar.vue`
|
||||
- **Endpoints provenance**: `/don_confiao/api/organizations/`, `/suppliers/`, `/countries/`, `/departments/`, `/municipalities/` (CRUD); vincular productos con `PATCH /don_confiao/api/products/<id>/` body `{"suppliers": [ids]}`; detalle de producto (con `suppliers`) via `GET /don_confiao/api/products/<id>/`
|
||||
- Los tests mockean `vis-network/standalone` (`vi.mock('vis-network/standalone', ...)`) o el propio `VisChart.vue`, y la API con `global.provide: { api }`
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
minVerticalSpacing: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
@@ -87,7 +91,28 @@
|
||||
network.setData({ nodes: nodesDataSet, edges: new DataSet(edges) })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
function applyMinVerticalSpacing (minSpacing) {
|
||||
if (!minSpacing || minSpacing <= 0) return
|
||||
const columns = new Map()
|
||||
for (const [id, position] of Object.entries(network.getPositions())) {
|
||||
const x = Math.round(position.x)
|
||||
if (!columns.has(x)) columns.set(x, [])
|
||||
columns.get(x).push({ id, x, y: position.y })
|
||||
}
|
||||
for (const column of columns.values()) {
|
||||
column.sort((a, b) => a.y - b.y)
|
||||
let previousY = null
|
||||
for (const item of column) {
|
||||
if (previousY !== null && item.y - previousY < minSpacing) {
|
||||
network.moveNode(item.id, item.x, previousY + minSpacing)
|
||||
}
|
||||
previousY = network.getPositions()[item.id].y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupNetwork () {
|
||||
if (network) network.destroy()
|
||||
network = new Network(containerRef.value, { nodes: [], edges: [] }, mergeOptions())
|
||||
render(props.nodes, props.edges)
|
||||
|
||||
@@ -100,15 +125,18 @@
|
||||
|
||||
network.once('stabilizationIterationsDone', () => {
|
||||
network.setOptions({ physics: { enabled: false } })
|
||||
applyMinVerticalSpacing(props.minVerticalSpacing)
|
||||
network.fit()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(setupNetwork)
|
||||
|
||||
watch(
|
||||
() => [props.nodes, props.edges],
|
||||
([nodes, edges]) => {
|
||||
if (!network) return
|
||||
render(nodes, edges)
|
||||
() => {
|
||||
if (!containerRef.value) return
|
||||
setupNetwork()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-alert
|
||||
v-if="!hasRelations"
|
||||
class="my-4"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
La información de proveedores y organizaciones de este pedido estará
|
||||
disponible próximamente.
|
||||
</v-alert>
|
||||
|
||||
<VisChart
|
||||
v-else
|
||||
data-test="supplier-org-chart"
|
||||
:edges="visEdges"
|
||||
:height="height"
|
||||
:nodes="visNodes"
|
||||
:options="chartOptions"
|
||||
@select="onSelect"
|
||||
/>
|
||||
|
||||
<ProvenanceDetailModal
|
||||
:selected="selected"
|
||||
:visible="modalVisible"
|
||||
@update:visible="modalVisible = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import VisChart from '@/components/graph/VisChart.vue'
|
||||
import ProvenanceDetailModal from './ProvenanceDetailModal.vue'
|
||||
import { buildSupplierOrganizationGraph, hasAnySupplier } from './provenance-graph'
|
||||
import { chartOptions, toVisEdges, toVisNodes } from './provenance-vis'
|
||||
|
||||
const props = defineProps({
|
||||
provenance: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const graph = computed(() => buildSupplierOrganizationGraph(props.provenance))
|
||||
const hasRelations = computed(() => hasAnySupplier(props.provenance))
|
||||
const visNodes = computed(() => toVisNodes(graph.value.nodes))
|
||||
const visEdges = computed(() => toVisEdges(graph.value.edges))
|
||||
const height = computed(() => Math.max(280, Math.min(560, graph.value.nodes.length * 72)))
|
||||
|
||||
const selected = ref(null)
|
||||
const modalVisible = ref(false)
|
||||
|
||||
function onSelect (node) {
|
||||
if (!node || node.kind === 'junction') return
|
||||
selected.value = node
|
||||
modalVisible.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-alert
|
||||
v-if="!hasRelations"
|
||||
class="my-4"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
La información de municipio, departamento y país de este pedido estará
|
||||
disponible próximamente.
|
||||
</v-alert>
|
||||
|
||||
<VisChart
|
||||
v-else
|
||||
data-test="territory-chart"
|
||||
:edges="visEdges"
|
||||
:height="height"
|
||||
:nodes="visNodes"
|
||||
:options="chartOptions"
|
||||
@select="onSelect"
|
||||
/>
|
||||
|
||||
<ProvenanceDetailModal
|
||||
:selected="selected"
|
||||
:visible="modalVisible"
|
||||
@update:visible="modalVisible = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import VisChart from '@/components/graph/VisChart.vue'
|
||||
import ProvenanceDetailModal from './ProvenanceDetailModal.vue'
|
||||
import { buildTerritoryGraph, hasAnyTerritory } from './provenance-graph'
|
||||
import { chartOptions, toVisEdges, toVisNodes } from './provenance-vis'
|
||||
|
||||
const props = defineProps({
|
||||
provenance: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const graph = computed(() => buildTerritoryGraph(props.provenance))
|
||||
const hasRelations = computed(() => hasAnyTerritory(props.provenance))
|
||||
const visNodes = computed(() => toVisNodes(graph.value.nodes))
|
||||
const visEdges = computed(() => toVisEdges(graph.value.edges))
|
||||
const height = computed(() => Math.max(280, Math.min(560, graph.value.nodes.length * 72)))
|
||||
|
||||
const selected = ref(null)
|
||||
const modalVisible = ref(false)
|
||||
|
||||
function onSelect (node) {
|
||||
if (!node || node.kind === 'junction') return
|
||||
selected.value = node
|
||||
modalVisible.value = true
|
||||
}
|
||||
</script>
|
||||
95
src/components/provenance/ProvenanceGraph.vue
Normal file
95
src/components/provenance/ProvenanceGraph.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-alert
|
||||
v-if="!hasSuppliers"
|
||||
class="my-4"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
La información de origen de los productos de este pedido estará disponible
|
||||
próximamente.
|
||||
</v-alert>
|
||||
|
||||
<template v-else>
|
||||
<div class="d-flex flex-wrap align-center ga-4 mb-2">
|
||||
<span class="text-body-2 font-weight-medium">Elementos a graficar:</span>
|
||||
<v-checkbox
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
v-model="selectedKinds"
|
||||
density="compact"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
v-if="visNodes.length === 0"
|
||||
class="my-2"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
>
|
||||
Selecciona al menos un elemento para graficar.
|
||||
</v-alert>
|
||||
|
||||
<VisChart
|
||||
v-if="visNodes.length > 0"
|
||||
data-test="provenance-chart"
|
||||
:edges="visEdges"
|
||||
:height="height"
|
||||
:min-vertical-spacing="minVerticalSpacing"
|
||||
:nodes="visNodes"
|
||||
:options="chartOptions"
|
||||
@select="onSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ProvenanceDetailModal
|
||||
:selected="selected"
|
||||
:visible="modalVisible"
|
||||
@update:visible="modalVisible = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import VisChart from '@/components/graph/VisChart.vue'
|
||||
import ProvenanceDetailModal from './ProvenanceDetailModal.vue'
|
||||
import { buildProvenanceGraph, hasAnySupplier } from './provenance-graph'
|
||||
import { chartOptions, toVisEdges, toVisNodes } from './provenance-vis'
|
||||
|
||||
const props = defineProps({
|
||||
provenance: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const options = [
|
||||
{ value: 'product', label: 'Productos' },
|
||||
{ value: 'supplier', label: 'Proveedores' },
|
||||
{ value: 'organization', label: 'Organizaciones' },
|
||||
{ value: 'municipality', label: 'Municipios' },
|
||||
{ value: 'department', label: 'Departamentos' },
|
||||
{ value: 'country', label: 'País' },
|
||||
]
|
||||
|
||||
const selectedKinds = ref(['product', 'supplier'])
|
||||
|
||||
const hasSuppliers = computed(() => hasAnySupplier(props.provenance))
|
||||
const graph = computed(() => buildProvenanceGraph(props.provenance, selectedKinds.value))
|
||||
const visNodes = computed(() => toVisNodes(graph.value.nodes))
|
||||
const visEdges = computed(() => toVisEdges(graph.value.edges))
|
||||
const height = computed(() => Math.max(280, Math.min(700, graph.value.nodes.length * 64)))
|
||||
const minVerticalSpacing = 2 * 24 + 22
|
||||
|
||||
const selected = ref(null)
|
||||
const modalVisible = ref(false)
|
||||
|
||||
function onSelect (node) {
|
||||
if (!node || node.kind === 'junction') return
|
||||
selected.value = node
|
||||
modalVisible.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -6,19 +6,12 @@
|
||||
Conoce quiénes producen los productos que compras y de qué territorios provienen.
|
||||
</p>
|
||||
|
||||
<h4 class="text-subtitle-1 font-weight-medium mb-2">Proveedores y organizaciones</h4>
|
||||
<ProductSupplierOrganizationChart :provenance="provenance" />
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<h4 class="text-subtitle-1 font-weight-medium mb-2">Departamento y municipio</h4>
|
||||
<ProductTerritoryChart :provenance="provenance" />
|
||||
<ProvenanceGraph :provenance="provenance" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProductSupplierOrganizationChart from './ProductSupplierOrganizationChart.vue'
|
||||
import ProductTerritoryChart from './ProductTerritoryChart.vue'
|
||||
import ProvenanceGraph from './ProvenanceGraph.vue'
|
||||
|
||||
defineProps({
|
||||
provenance: {
|
||||
|
||||
@@ -8,11 +8,19 @@
|
||||
* - Con varios proveedores para un producto se inserta un nodo de disyunción
|
||||
* (junction): sólida hasta él y discontinua hacia cada proveedor.
|
||||
*
|
||||
* `buildProvenanceGraph` permite elegir los niveles a graficar
|
||||
* (product, supplier, organization, municipality, department, country);
|
||||
* los niveles no solicitados se omiten conectando el nivel previo con el
|
||||
* siguiente.
|
||||
*
|
||||
* El modelo devuelto es agnóstico de la herramienta de renderizado:
|
||||
* node: { id, kind, label, image, entity }
|
||||
* edge: { from, to, certain }
|
||||
*/
|
||||
|
||||
const DEFAULT_KINDS = ['product', 'supplier']
|
||||
const TERRITORY_ORDER = ['municipality', 'department', 'country']
|
||||
|
||||
function makeCollector () {
|
||||
const nodes = []
|
||||
const edges = []
|
||||
@@ -57,65 +65,76 @@ function distinctValues (items) {
|
||||
return [...seen.values()]
|
||||
}
|
||||
|
||||
function connectSuppliers (collector, product, relations) {
|
||||
if (relations.length === 1) {
|
||||
const supplier = collector.addNode('supplier', relations[0].supplier)
|
||||
collector.addEdge(product.id, supplier.id, true)
|
||||
return
|
||||
}
|
||||
const junction = collector.addNode('junction', { id: product.entity.id, name: '' })
|
||||
collector.addEdge(product.id, junction.id, true)
|
||||
for (const rel of relations) {
|
||||
const supplier = collector.addNode('supplier', rel.supplier)
|
||||
collector.addEdge(junction.id, supplier.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
function isCertain (values) {
|
||||
return values.length <= 1
|
||||
}
|
||||
|
||||
export function buildSupplierOrganizationGraph (provenance) {
|
||||
const collector = makeCollector()
|
||||
for (const entry of provenance || []) {
|
||||
if (!entry.product) continue
|
||||
const product = collector.addNode('product', entry.product)
|
||||
const relations = (entry.suppliers || []).filter(rel => rel.supplier)
|
||||
if (relations.length === 0) continue
|
||||
connectSuppliers(collector, product, relations)
|
||||
|
||||
const organizationCertain = isCertain(distinctValues(relations.map(rel => rel.organization)))
|
||||
for (const rel of relations) {
|
||||
if (!rel.organization) continue
|
||||
const organization = collector.addNode('organization', rel.organization)
|
||||
collector.addEdge(`supplier:${rel.supplier.id}`, organization.id, organizationCertain)
|
||||
}
|
||||
function wirePath (collector, path, certainties) {
|
||||
for (let index = 0; index < path.length - 1; index++) {
|
||||
const to = path[index + 1]
|
||||
const kind = to.split(':')[0]
|
||||
collector.addEdge(path[index], to, certainties[kind])
|
||||
}
|
||||
return { nodes: collector.nodes, edges: collector.edges }
|
||||
}
|
||||
|
||||
export function buildTerritoryGraph (provenance) {
|
||||
export function buildProvenanceGraph (provenance, kinds = DEFAULT_KINDS) {
|
||||
const active = new Set(kinds)
|
||||
const collector = makeCollector()
|
||||
|
||||
for (const entry of provenance || []) {
|
||||
if (!entry.product) continue
|
||||
const product = collector.addNode('product', entry.product)
|
||||
const relations = (entry.suppliers || []).filter(rel => rel.supplier)
|
||||
if (relations.length === 0) continue
|
||||
connectSuppliers(collector, product, relations)
|
||||
|
||||
const municipalityCertain = isCertain(distinctValues(relations.map(rel => rel.municipality)))
|
||||
const departmentCertain = isCertain(distinctValues(relations.map(rel => rel.department)))
|
||||
const product = active.has('product') ? collector.addNode('product', entry.product) : null
|
||||
const suppliersActive = active.has('supplier')
|
||||
|
||||
for (const rel of relations) {
|
||||
if (!rel.municipality) continue
|
||||
const municipality = collector.addNode('municipality', rel.municipality)
|
||||
collector.addEdge(`supplier:${rel.supplier.id}`, municipality.id, municipalityCertain)
|
||||
const certainties = {
|
||||
municipality: isCertain(distinctValues(relations.map(rel => rel.municipality))),
|
||||
department: isCertain(distinctValues(relations.map(rel => rel.department))),
|
||||
country: isCertain(distinctValues(relations.map(rel => rel.country))),
|
||||
organization: isCertain(distinctValues(relations.map(rel => rel.organization))),
|
||||
}
|
||||
|
||||
for (const rel of relations) {
|
||||
if (!rel.municipality || !rel.department) continue
|
||||
const municipality = collector.addNode('municipality', rel.municipality)
|
||||
const department = collector.addNode('department', rel.department)
|
||||
collector.addEdge(municipality.id, department.id, departmentCertain)
|
||||
if (suppliersActive) collector.addNode('supplier', rel.supplier)
|
||||
if (active.has('municipality') && rel.municipality) collector.addNode('municipality', rel.municipality)
|
||||
if (active.has('department') && rel.department) collector.addNode('department', rel.department)
|
||||
if (active.has('country') && rel.country) collector.addNode('country', rel.country)
|
||||
if (active.has('organization') && rel.organization) collector.addNode('organization', rel.organization)
|
||||
}
|
||||
|
||||
if (suppliersActive && product) {
|
||||
if (relations.length === 1) {
|
||||
collector.addEdge(product.id, `supplier:${relations[0].supplier.id}`, true)
|
||||
} else {
|
||||
const junction = collector.addNode('junction', { id: entry.product.id, name: '' })
|
||||
collector.addEdge(product.id, junction.id, true)
|
||||
for (const rel of relations) {
|
||||
collector.addEdge(junction.id, `supplier:${rel.supplier.id}`, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const rel of relations) {
|
||||
const path = []
|
||||
if (suppliersActive) {
|
||||
path.push(`supplier:${rel.supplier.id}`)
|
||||
} else if (product) {
|
||||
path.push(product.id)
|
||||
}
|
||||
for (const level of TERRITORY_ORDER) {
|
||||
if (active.has(level) && rel[level]) path.push(`${level}:${rel[level].id}`)
|
||||
}
|
||||
wirePath(collector, path, certainties)
|
||||
}
|
||||
|
||||
if (active.has('organization')) {
|
||||
for (const rel of relations) {
|
||||
if (!rel.organization) continue
|
||||
const source = suppliersActive ? `supplier:${rel.supplier.id}` : (product ? product.id : null)
|
||||
if (source) collector.addEdge(source, `organization:${rel.organization.id}`, certainties.organization)
|
||||
}
|
||||
}
|
||||
}
|
||||
return { nodes: collector.nodes, edges: collector.edges }
|
||||
@@ -124,11 +143,3 @@ export function buildTerritoryGraph (provenance) {
|
||||
export function hasAnySupplier (provenance) {
|
||||
return (provenance || []).some(entry => (entry.suppliers || []).length > 0)
|
||||
}
|
||||
|
||||
export function hasAnyTerritory (provenance) {
|
||||
return (provenance || []).some(entry =>
|
||||
(entry.suppliers || []).some(rel =>
|
||||
rel.municipality || rel.department
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ const COLORS = {
|
||||
const PRODUCT_SPACING = 110
|
||||
const COLUMN_X = {
|
||||
product: -240,
|
||||
junction: -120,
|
||||
supplier: 0,
|
||||
organization: 240,
|
||||
junction: -160,
|
||||
supplier: -80,
|
||||
organization: 80,
|
||||
municipality: 240,
|
||||
department: 480,
|
||||
department: 400,
|
||||
country: 560,
|
||||
}
|
||||
|
||||
export function toVisNodes (nodes) {
|
||||
|
||||
@@ -10,6 +10,8 @@ const vis = vi.hoisted(() => {
|
||||
setOptions: vi.fn(),
|
||||
fit: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
getPositions: vi.fn(() => ({})),
|
||||
moveNode: vi.fn(),
|
||||
}
|
||||
const dataSetInstance = {
|
||||
get: vi.fn(),
|
||||
@@ -96,12 +98,46 @@ describe('VisChart', () => {
|
||||
expect(vis.networkInstance.fit).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('actualiza la red cuando cambian los nodos o aristas', async () => {
|
||||
it('separa verticalmente los nodos de una misma columna tras estabilizar', () => {
|
||||
vis.networkInstance.getPositions.mockReturnValue({
|
||||
'supplier:5': { x: 0, y: 100 },
|
||||
'supplier:6': { x: 0, y: 112 },
|
||||
'municipality:7': { x: 240, y: 100 },
|
||||
'product:1': { x: -240, y: 60 },
|
||||
})
|
||||
mount(VisChart, { props: { nodes: [], edges: [], minVerticalSpacing: 70 } })
|
||||
|
||||
const onStabilized = handlerFor(vis.networkInstance.once, 'stabilizationIterationsDone')
|
||||
onStabilized()
|
||||
|
||||
expect(vis.networkInstance.moveNode).toHaveBeenCalledWith('supplier:6', 0, 170)
|
||||
expect(vis.networkInstance.moveNode).not.toHaveBeenCalledWith('supplier:5', 0, expect.anything())
|
||||
expect(vis.networkInstance.moveNode).not.toHaveBeenCalledWith('municipality:7', 240, expect.anything())
|
||||
expect(vis.networkInstance.moveNode).not.toHaveBeenCalledWith('product:1', -240, expect.anything())
|
||||
})
|
||||
|
||||
it('no separa nodos cuando no se indica espaciado vertical mínimo', () => {
|
||||
vis.networkInstance.getPositions.mockReturnValue({
|
||||
'supplier:5': { x: 0, y: 100 },
|
||||
'supplier:6': { x: 0, y: 112 },
|
||||
})
|
||||
mount(VisChart, { props: { nodes: [], edges: [] } })
|
||||
|
||||
const onStabilized = handlerFor(vis.networkInstance.once, 'stabilizationIterationsDone')
|
||||
onStabilized()
|
||||
|
||||
expect(vis.networkInstance.moveNode).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('vuelve a estabilizar el grafo cuando cambian los nodos o aristas', async () => {
|
||||
const wrapper = mount(VisChart, { props: { nodes: [{ id: 'product:1' }], edges: [] } })
|
||||
expect(vis.networkInstance.setData).toHaveBeenCalledTimes(1)
|
||||
expect(vis.Network).toHaveBeenCalledTimes(1)
|
||||
expect(vis.networkInstance.destroy).not.toHaveBeenCalled()
|
||||
|
||||
await wrapper.setProps({ nodes: [{ id: 'product:2' }], edges: [{ from: 'product:2', to: 'supplier:5' }] })
|
||||
|
||||
expect(vis.networkInstance.destroy).toHaveBeenCalled()
|
||||
expect(vis.Network).toHaveBeenCalledTimes(2)
|
||||
expect(vis.networkInstance.setData).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ProvenanceDetailModal from '@/components/provenance/ProvenanceDetailModal.vue'
|
||||
import ProductSupplierOrganizationChart from '@/components/provenance/ProductSupplierOrganizationChart.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
vi.mock('@/components/graph/VisChart.vue', () => ({
|
||||
default: {
|
||||
name: 'VisChart',
|
||||
template: '<div class="vis-chart-stub" />',
|
||||
props: ['nodes', 'edges', 'height', 'options'],
|
||||
},
|
||||
}))
|
||||
|
||||
const provenance = [
|
||||
{
|
||||
product: {
|
||||
id: 1,
|
||||
name: 'Panela regional por Kg',
|
||||
catalogue_images: ['http://localhost/media/panela.jpg'],
|
||||
},
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa', description: 'Cooperativa' },
|
||||
organization: { id: 3, name: 'Red de Economía Solidaria', description: 'Red' },
|
||||
municipality: null,
|
||||
department: null,
|
||||
country: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
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 },
|
||||
global: { plugins: [vuetify] },
|
||||
})
|
||||
}
|
||||
|
||||
function visChart (wrapper) {
|
||||
return wrapper.findComponent({ name: 'VisChart' })
|
||||
}
|
||||
|
||||
describe('ProductSupplierOrganizationChart', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renderiza el gráfico con nodos de productos, proveedores y organizaciones', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const chart = visChart(wrapper)
|
||||
expect(chart.exists()).toBe(true)
|
||||
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('incluye la etiqueta del nodo y la imagen circular para quien tiene foto', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
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', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
expect(wrapper.text()).not.toContain('próximamente')
|
||||
})
|
||||
|
||||
it('muestra mensaje de próximamente cuando ningún producto tiene proveedores', () => {
|
||||
const wrapper = mountChart({
|
||||
provenance: [{ product: { id: 1, name: 'Panela' }, suppliers: [] }],
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
it('alinea los productos en una columna vertical fija y centrada', () => {
|
||||
const provenance = [
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'A' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 6, name: 'B' } }] },
|
||||
{ product: { id: 3, name: 'Queso' }, suppliers: [{ supplier: { id: 7, name: 'C' } }] },
|
||||
]
|
||||
const wrapper = mountChart({ provenance })
|
||||
|
||||
const products = visChart(wrapper).props('nodes').filter(node => node.kind === 'product')
|
||||
expect(products).toHaveLength(3)
|
||||
expect(new Set(products.map(node => node.x)).size).toBe(1)
|
||||
expect(products.map(node => node.y)).toEqual([-110, 0, 110])
|
||||
products.forEach(node => {
|
||||
expect(node.fixed).toEqual({ x: true, y: true })
|
||||
})
|
||||
})
|
||||
|
||||
it('alinea proveedores y organizaciones en columnas por nivel', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const supplier = visChart(wrapper).props('nodes').find(node => node.id === 'supplier:5')
|
||||
expect(supplier.x).toBe(0)
|
||||
expect(supplier.fixed).toEqual({ x: true, y: false })
|
||||
expect(supplier.y).toBeUndefined()
|
||||
|
||||
const organization = visChart(wrapper).props('nodes').find(node => node.id === 'organization:3')
|
||||
expect(organization.x).toBe(240)
|
||||
expect(organization.fixed).toEqual({ x: true, y: false })
|
||||
})
|
||||
|
||||
it('el punto de disyunción queda a la misma altura que su producto', () => {
|
||||
const wrapper = mountChart({ provenance: provenanceWithTwoSuppliers })
|
||||
|
||||
const junction = visChart(wrapper).props('nodes').find(node => node.id === 'junction:1')
|
||||
const product = visChart(wrapper).props('nodes').find(node => node.id === 'product:1')
|
||||
expect(junction.x).toBe(-120)
|
||||
expect(junction.y).toBe(product.y)
|
||||
expect(junction.fixed).toEqual({ x: true, y: true })
|
||||
})
|
||||
})
|
||||
@@ -1,146 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ProvenanceDetailModal from '@/components/provenance/ProvenanceDetailModal.vue'
|
||||
import ProductTerritoryChart from '@/components/provenance/ProductTerritoryChart.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
vi.mock('@/components/graph/VisChart.vue', () => ({
|
||||
default: {
|
||||
name: 'VisChart',
|
||||
template: '<div class="vis-chart-stub" />',
|
||||
props: ['nodes', 'edges', 'height', 'options'],
|
||||
},
|
||||
}))
|
||||
|
||||
const provenance = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela regional por Kg' },
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa' },
|
||||
organization: null,
|
||||
municipality: { id: 7, name: 'La Mesa' },
|
||||
department: { id: 2, name: 'Cundinamarca' },
|
||||
country: { id: 1, name: 'Colombia', code: 'CO' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
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 },
|
||||
global: { plugins: [vuetify] },
|
||||
})
|
||||
}
|
||||
|
||||
function visChart (wrapper) {
|
||||
return wrapper.findComponent({ name: 'VisChart' })
|
||||
}
|
||||
|
||||
describe('ProductTerritoryChart', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renderiza el gráfico con municipio y departamento, sin país', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const chart = visChart(wrapper)
|
||||
expect(chart.exists()).toBe(true)
|
||||
const nodeIds = chart.props('nodes').map(node => node.id)
|
||||
expect(nodeIds).toContain('product:1')
|
||||
expect(nodeIds).toContain('supplier:5')
|
||||
expect(nodeIds).toContain('municipality:7')
|
||||
expect(nodeIds).toContain('department:2')
|
||||
expect(nodeIds).not.toContain('country:1')
|
||||
})
|
||||
|
||||
it('incluye la etiqueta del nodo y el color según el tipo', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
it('con varios productos del mismo proveedor solo traza una línea al municipio', () => {
|
||||
const provenanceWithRepeatedSupplier = [
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
{ product: { id: 3, name: 'Queso' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
]
|
||||
const wrapper = mountChart({ provenance: provenanceWithRepeatedSupplier })
|
||||
|
||||
const chart = visChart(wrapper)
|
||||
const edges = chart.props('edges')
|
||||
expect(edges.filter(edge => edge.from === 'supplier:5' && edge.to === 'municipality:7')).toHaveLength(1)
|
||||
expect(chart.props('nodes').filter(node => node.id === 'municipality:7')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('muestra mensaje de próximamente cuando ningún proveedor tiene territorio', () => {
|
||||
const wrapper = mountChart({
|
||||
provenance: [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: null, department: null, country: null }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
it('alinea municipios y departamentos en columnas por nivel', () => {
|
||||
const wrapper = mountChart()
|
||||
|
||||
const nodes = visChart(wrapper).props('nodes')
|
||||
expect(nodes.find(node => node.id === 'supplier:5').x).toBe(0)
|
||||
expect(nodes.find(node => node.id === 'municipality:7').x).toBe(240)
|
||||
expect(nodes.find(node => node.id === 'department:2').x).toBe(480)
|
||||
})
|
||||
})
|
||||
154
tests/unit/components/provenance/ProvenanceGraph.spec.js
Normal file
154
tests/unit/components/provenance/ProvenanceGraph.spec.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import ProvenanceDetailModal from '@/components/provenance/ProvenanceDetailModal.vue'
|
||||
import ProvenanceGraph from '@/components/provenance/ProvenanceGraph.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
vi.mock('@/components/graph/VisChart.vue', () => ({
|
||||
default: {
|
||||
name: 'VisChart',
|
||||
template: '<div class="vis-chart-stub" />',
|
||||
props: ['nodes', 'edges', 'height', 'options', 'minVerticalSpacing'],
|
||||
},
|
||||
}))
|
||||
|
||||
const provenance = [
|
||||
{
|
||||
product: {
|
||||
id: 1,
|
||||
name: 'Panela regional por Kg',
|
||||
catalogue_images: ['http://localhost/media/panela.jpg'],
|
||||
},
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa' },
|
||||
organization: { id: 3, name: 'Red de Economía Solidaria' },
|
||||
municipality: { id: 7, name: 'La Mesa' },
|
||||
department: { id: 2, name: 'Cundinamarca' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function mountGraph (props = {}) {
|
||||
return mount(ProvenanceGraph, {
|
||||
props: { provenance, ...props },
|
||||
global: { plugins: [vuetify] },
|
||||
})
|
||||
}
|
||||
|
||||
function visChart (wrapper) {
|
||||
return wrapper.findComponent({ name: 'VisChart' })
|
||||
}
|
||||
|
||||
function checkbox (wrapper, label) {
|
||||
return wrapper.findAllComponents({ name: 'VCheckbox' }).find(cb => cb.props('label') === label)
|
||||
}
|
||||
|
||||
async function setKinds (wrapper, kinds) {
|
||||
checkbox(wrapper, 'Productos').vm.$emit('update:modelValue', kinds)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
describe('ProvenanceGraph', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('por defecto grafica solo productos y proveedores', () => {
|
||||
const wrapper = mountGraph()
|
||||
|
||||
const nodeKinds = visChart(wrapper).props('nodes').map(node => node.kind)
|
||||
expect(nodeKinds).toEqual(['product', 'supplier'])
|
||||
const edges = visChart(wrapper).props('edges')
|
||||
expect(edges.find(edge => edge.from === 'product:1' && edge.to === 'supplier:5').dashes).toBe(false)
|
||||
})
|
||||
|
||||
it('pide separación vertical mínima equivalente al círculo más una línea de label', () => {
|
||||
const wrapper = mountGraph()
|
||||
|
||||
expect(visChart(wrapper).props('minVerticalSpacing')).toBe(70)
|
||||
})
|
||||
|
||||
it('muestra un checkbox por cada nivel con productos y proveedores activos', () => {
|
||||
const wrapper = mountGraph()
|
||||
|
||||
const labels = wrapper.findAllComponents({ name: 'VCheckbox' }).map(cb => cb.props('label'))
|
||||
expect(labels).toEqual(['Productos', 'Proveedores', 'Organizaciones', 'Municipios', 'Departamentos', 'País'])
|
||||
})
|
||||
|
||||
it('al activar municipios agrega nodos de municipio y la arista proveedor→municipio', async () => {
|
||||
const wrapper = mountGraph()
|
||||
|
||||
await setKinds(wrapper, ['product', 'supplier', 'municipality'])
|
||||
|
||||
const nodeIds = visChart(wrapper).props('nodes').map(node => node.id)
|
||||
expect(nodeIds).toContain('municipality:7')
|
||||
const edges = visChart(wrapper).props('edges')
|
||||
expect(edges.find(edge => edge.from === 'supplier:5' && edge.to === 'municipality:7').dashes).toBe(false)
|
||||
})
|
||||
|
||||
it('con país activo crea el nodo país', async () => {
|
||||
const wrapper = mountGraph()
|
||||
|
||||
await setKinds(wrapper, ['product', 'supplier', 'country'])
|
||||
|
||||
expect(visChart(wrapper).props('nodes').map(node => node.id)).toContain('country:1')
|
||||
})
|
||||
|
||||
it('al desactivar productos no se crean nodos de producto', async () => {
|
||||
const wrapper = mountGraph()
|
||||
|
||||
await setKinds(wrapper, ['supplier', 'municipality', 'department'])
|
||||
|
||||
const nodeKinds = visChart(wrapper).props('nodes').map(node => node.kind)
|
||||
expect(nodeKinds).not.toContain('product')
|
||||
expect(nodeKinds).not.toContain('junction')
|
||||
})
|
||||
|
||||
it('avisa cuando no queda ningún nivel seleccionado', async () => {
|
||||
const wrapper = mountGraph()
|
||||
|
||||
await setKinds(wrapper, [])
|
||||
|
||||
expect(visChart(wrapper).exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('Selecciona al menos un elemento')
|
||||
})
|
||||
|
||||
it('muestra mensaje de próximamente cuando ningún producto tiene proveedores', () => {
|
||||
const wrapper = mountGraph({
|
||||
provenance: [{ product: { id: 1, name: 'Panela' }, suppliers: [] }],
|
||||
})
|
||||
|
||||
expect(visChart(wrapper).exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('próximamente')
|
||||
})
|
||||
|
||||
it('abre el modal de detalle al seleccionar un proveedor', async () => {
|
||||
const wrapper = mountGraph()
|
||||
|
||||
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 provenanceWithTwoSuppliers = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [
|
||||
{ supplier: { id: 5, name: 'A' }, municipality: { id: 7, name: 'La Mesa' } },
|
||||
{ supplier: { id: 6, name: 'B' }, municipality: { id: 8, name: 'San Antonio' } },
|
||||
],
|
||||
},
|
||||
]
|
||||
const wrapper = mountGraph({ provenance: provenanceWithTwoSuppliers })
|
||||
|
||||
await visChart(wrapper).vm.$emit('select', { id: 'junction:1', kind: 'junction', label: '' })
|
||||
|
||||
expect(wrapper.findComponent(ProvenanceDetailModal).props('visible')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ProductSupplierOrganizationChart from '@/components/provenance/ProductSupplierOrganizationChart.vue'
|
||||
import ProductTerritoryChart from '@/components/provenance/ProductTerritoryChart.vue'
|
||||
import ProvenanceGraph from '@/components/provenance/ProvenanceGraph.vue'
|
||||
import ProvenanceSection from '@/components/provenance/ProvenanceSection.vue'
|
||||
import vuetify from '@/plugins/vuetify'
|
||||
|
||||
@@ -39,30 +38,25 @@ describe('ProvenanceSection', () => {
|
||||
it('no renderiza nada cuando no hay provenance', () => {
|
||||
const wrapper = mountSection({ provenance: null })
|
||||
|
||||
expect(wrapper.findComponent(ProductSupplierOrganizationChart).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(ProductTerritoryChart).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('no renderiza nada cuando provenance es una lista vacía', () => {
|
||||
const wrapper = mountSection({ provenance: [] })
|
||||
|
||||
expect(wrapper.findComponent(ProductSupplierOrganizationChart).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(ProductTerritoryChart).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('muestra los dos gráficos con sus títulos cuando hay provenance', () => {
|
||||
it('muestra el gráfico unificado con su título cuando hay provenance', () => {
|
||||
const wrapper = mountSection()
|
||||
|
||||
expect(wrapper.findComponent(ProductSupplierOrganizationChart).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(ProductTerritoryChart).exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Proveedores y organizaciones')
|
||||
expect(wrapper.text()).toContain('Departamento y municipio')
|
||||
expect(wrapper.findComponent(ProvenanceGraph).exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Origen e historia de los productos')
|
||||
})
|
||||
|
||||
it('pasa el provenance a ambos gráficos', () => {
|
||||
it('pasa el provenance al gráfico', () => {
|
||||
const wrapper = mountSection()
|
||||
|
||||
expect(wrapper.findComponent(ProductSupplierOrganizationChart).props('provenance')).toStrictEqual(provenance)
|
||||
expect(wrapper.findComponent(ProductTerritoryChart).props('provenance')).toStrictEqual(provenance)
|
||||
expect(wrapper.findComponent(ProvenanceGraph).props('provenance')).toStrictEqual(provenance)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildSupplierOrganizationGraph,
|
||||
buildTerritoryGraph,
|
||||
buildProvenanceGraph,
|
||||
hasAnySupplier,
|
||||
hasAnyTerritory,
|
||||
} from '@/components/provenance/provenance-graph'
|
||||
|
||||
const singleSupplier = [
|
||||
const ALL_KINDS = ['product', 'supplier', 'organization', 'municipality', 'department', 'country']
|
||||
|
||||
const singleRelation = [
|
||||
{
|
||||
product: {
|
||||
id: 1,
|
||||
@@ -15,39 +15,41 @@ const singleSupplier = [
|
||||
},
|
||||
suppliers: [
|
||||
{
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa', description: 'Cooperativa' },
|
||||
organization: { id: 3, name: 'Red de Economía Solidaria', description: 'Red' },
|
||||
supplier: { id: 5, name: 'Asociación Agropecuaria La Mesa' },
|
||||
organization: { id: 3, name: 'Red de Economía Solidaria' },
|
||||
municipality: { id: 7, name: 'La Mesa' },
|
||||
department: { id: 2, name: 'Cundinamarca' },
|
||||
country: { id: 1, name: 'Colombia', code: 'CO' },
|
||||
country: { id: 1, name: 'Colombia' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
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 twoSuppliers (overrides = {}) {
|
||||
return [
|
||||
{
|
||||
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)
|
||||
@@ -55,7 +57,7 @@ function edgeOf (graph, from, to) {
|
||||
|
||||
describe('hasAnySupplier', () => {
|
||||
it('es true cuando algún producto tiene proveedores', () => {
|
||||
expect(hasAnySupplier(singleSupplier)).toBe(true)
|
||||
expect(hasAnySupplier(singleRelation)).toBe(true)
|
||||
})
|
||||
|
||||
it('es false cuando todos los productos están sin proveedores', () => {
|
||||
@@ -68,115 +70,82 @@ describe('hasAnySupplier', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasAnyTerritory', () => {
|
||||
it('es true cuando algún proveedor tiene territorio', () => {
|
||||
expect(hasAnyTerritory(singleSupplier)).toBe(true)
|
||||
})
|
||||
describe('buildProvenanceGraph', () => {
|
||||
it('por defecto solo grafica productos y proveedores', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation)
|
||||
|
||||
it('es false cuando todos los territorios vienen en null', () => {
|
||||
const noTerritory = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: null, municipality: null, department: null, country: null }],
|
||||
},
|
||||
]
|
||||
expect(hasAnyTerritory(noTerritory)).toBe(false)
|
||||
})
|
||||
|
||||
it('es false cuando no hay suppliers', () => {
|
||||
const noSuppliers = [{ product: { id: 1, name: 'Panela' }, suppliers: [] }]
|
||||
expect(hasAnyTerritory(noSuppliers)).toBe(false)
|
||||
})
|
||||
|
||||
it('es true cuando hay municipio aunque el país venga null', () => {
|
||||
const data = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: null, country: null }],
|
||||
},
|
||||
]
|
||||
expect(hasAnyTerritory(data)).toBe(true)
|
||||
})
|
||||
|
||||
it('es false cuando solo hay país', () => {
|
||||
const data = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: null, department: null, country: { id: 1, name: 'Colombia' } }],
|
||||
},
|
||||
]
|
||||
expect(hasAnyTerritory(data)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSupplierOrganizationGraph', () => {
|
||||
it('con un único proveedor conecta con arista cierta y sin junction', () => {
|
||||
const graph = buildSupplierOrganizationGraph(singleSupplier)
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
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(singleSupplier)
|
||||
const product = graph.nodes.find(n => n.id === 'product:1')
|
||||
|
||||
expect(product.image).toBe('http://localhost/media/panela.jpg')
|
||||
expect(product.label).toBe('Panela regional por Kg')
|
||||
expect(product.entity.kind).toBe('product')
|
||||
expect(product.entity.name).toBe('Panela regional por Kg')
|
||||
})
|
||||
|
||||
it('omite el nodo de organización cuando la relación es null', () => {
|
||||
const data = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: null }],
|
||||
},
|
||||
]
|
||||
const graph = buildSupplierOrganizationGraph(data)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).not.toContain('organization:')
|
||||
expect(graph.nodes.map(n => n.kind)).toEqual(['product', 'supplier'])
|
||||
expect(graph.edges).toEqual([{ from: 'product:1', to: 'supplier:5', certain: true }])
|
||||
})
|
||||
|
||||
it('no duplica nodos ni aristas cuando se repiten entre productos', () => {
|
||||
const data = [
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: { id: 3, name: 'Red' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, organization: { id: 3, name: 'Red' } }] },
|
||||
]
|
||||
const graph = buildSupplierOrganizationGraph(data)
|
||||
it('con todos los niveles grafica la cadena completa y la organización', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ALL_KINDS)
|
||||
|
||||
expect(graph.nodes.filter(n => n.id === 'supplier:5')).toHaveLength(1)
|
||||
expect(graph.nodes.filter(n => n.id === 'organization:3')).toHaveLength(1)
|
||||
expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'organization:3')).toHaveLength(1)
|
||||
expect(graph.edges).toHaveLength(3)
|
||||
expect(edgeOf(graph, 'product:1', 'supplier:5')).toEqual({ from: 'product:1', to: 'supplier:5', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3').certain).toBe(true)
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toEqual({ from: 'supplier:5', 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('con varios proveedores crea el punto de disyunción y la duda se propaga', () => {
|
||||
const graph = buildProvenanceGraph(twoSuppliers(), ALL_KINDS)
|
||||
|
||||
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').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'junction:1', 'supplier:6').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(false)
|
||||
expect(edgeOf(graph, 'department:2', 'country:1').certain).toBe(true)
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('omite los niveles no solicitados conectando el nivel previo con el siguiente', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ['product', 'supplier', 'department'])
|
||||
|
||||
expect(graph.nodes.map(n => n.kind)).toEqual(['product', 'supplier', 'department'])
|
||||
expect(edgeOf(graph, 'supplier:5', 'department:2')).toEqual({ from: 'supplier:5', to: 'department:2', certain: true })
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('sin proveedores activos conecta el producto con el siguiente nivel', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ['product', 'municipality', 'department'])
|
||||
|
||||
expect(edgeOf(graph, 'product:1', 'municipality:7')).toEqual({ from: 'product:1', to: 'municipality:7', certain: true })
|
||||
expect(edgeOf(graph, 'municipality:7', 'department:2').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('sin productos activos los proveedores quedan como raíces sin junction', () => {
|
||||
const graph = buildProvenanceGraph(twoSuppliers(), ['supplier', 'municipality'])
|
||||
|
||||
expect(graph.nodes.some(n => n.kind === 'product')).toBe(false)
|
||||
expect(graph.nodes.some(n => n.kind === 'junction')).toBe(false)
|
||||
expect(edgeOf(graph, 'supplier:5', 'municipality:7').certain).toBe(false)
|
||||
})
|
||||
|
||||
it('con país activo crea el nodo país aunque se omitan niveles intermedios', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ['product', 'supplier', 'country'])
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).toContain('country:1')
|
||||
expect(edgeOf(graph, 'supplier:5', 'country:1').certain).toBe(true)
|
||||
})
|
||||
|
||||
it('no crea nodos de país si no se solicita', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation, ['product', 'supplier', 'municipality'])
|
||||
|
||||
expect(graph.nodes.some(n => n.kind === 'country')).toBe(false)
|
||||
})
|
||||
|
||||
it('no duplica aristas cuando varios productos comparten proveedor y municipio', () => {
|
||||
const data = [
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'A' }, municipality: { id: 7, name: 'La Mesa' }, department: { id: 2, name: 'Cundinamarca' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'A' }, municipality: { id: 7, name: 'La Mesa' }, department: { id: 2, name: 'Cundinamarca' } }] },
|
||||
]
|
||||
const graph = buildProvenanceGraph(data, ['product', 'supplier', 'municipality'])
|
||||
|
||||
expect(graph.nodes.filter(n => n.id === 'municipality:7')).toHaveLength(1)
|
||||
expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'municipality:7')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('si el mismo par de nodos repite con distinta certeza, gana la duda', () => {
|
||||
@@ -184,98 +153,24 @@ describe('buildSupplierOrganizationGraph', () => {
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'A' }, organization: { id: 3, name: 'Red' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'A' }, organization: { id: 3, name: 'Red' } }, { supplier: { id: 6, name: 'B' }, organization: { id: 4, name: 'Coop' } }] },
|
||||
]
|
||||
const graph = buildSupplierOrganizationGraph(data)
|
||||
const graph = buildProvenanceGraph(data, ['product', 'supplier', 'organization'])
|
||||
|
||||
expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'organization:3')).toHaveLength(1)
|
||||
expect(edgeOf(graph, 'supplier:5', 'organization:3').certain).toBe(false)
|
||||
})
|
||||
|
||||
it('no incluye posiciones de layout', () => {
|
||||
const graph = buildSupplierOrganizationGraph(singleSupplier)
|
||||
it('incluye la imagen y el detalle de la entidad del producto', () => {
|
||||
const graph = buildProvenanceGraph(singleRelation)
|
||||
const product = graph.nodes.find(n => n.id === 'product:1')
|
||||
|
||||
expect(product.x).toBeUndefined()
|
||||
expect(product.y).toBeUndefined()
|
||||
expect(product.image).toBe('http://localhost/media/panela.jpg')
|
||||
expect(product.entity.kind).toBe('product')
|
||||
})
|
||||
|
||||
it('devuelve un grafo vacío cuando no hay provenance', () => {
|
||||
const graph = buildSupplierOrganizationGraph([])
|
||||
const graph = buildProvenanceGraph([])
|
||||
|
||||
expect(graph.nodes).toEqual([])
|
||||
expect(graph.edges).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildTerritoryGraph', () => {
|
||||
it('con un único proveedor toda la cadena es cierta', () => {
|
||||
const graph = buildTerritoryGraph(singleSupplier)
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
it('no incluye nodos ni aristas de país', () => {
|
||||
const graph = buildTerritoryGraph(singleSupplier)
|
||||
|
||||
expect(graph.nodes.map(n => n.id)).not.toContain('country:1')
|
||||
expect(graph.edges.some(edge => edge.from.startsWith('country:') || edge.to.startsWith('country:'))).toBe(false)
|
||||
})
|
||||
|
||||
it('con varios productos del mismo proveedor solo traza una línea hacia el municipio', () => {
|
||||
const data = [
|
||||
{ product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
{ product: { id: 2, name: 'Arroz' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
{ product: { id: 3, name: 'Queso' }, suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara' }, department: { id: 2, name: 'Antioquia' } }] },
|
||||
]
|
||||
const graph = buildTerritoryGraph(data)
|
||||
|
||||
expect(graph.edges.filter(e => e.from === 'supplier:5' && e.to === 'municipality:7')).toHaveLength(1)
|
||||
expect(graph.nodes.filter(n => n.id === 'municipality:7')).toHaveLength(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)
|
||||
})
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
it('municipios y departamentos distintos mantienen la duda', () => {
|
||||
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)
|
||||
})
|
||||
|
||||
it('no crea nodos de territorio cuando el proveedor no tiene municipio', () => {
|
||||
const data = [
|
||||
{
|
||||
product: { id: 1, name: 'Panela' },
|
||||
suppliers: [{ supplier: { id: 5, name: 'La Mesa' }, municipality: null, department: null, country: null }],
|
||||
},
|
||||
]
|
||||
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', certain: true }])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user