feat/49-provenance #54
9
package-lock.json
generated
9
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"@mdi/font": "7.4.47",
|
||||
"axios": "^1.13.5",
|
||||
"core-js": "^3.37.1",
|
||||
"cytoscape": "^3.34.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"roboto-fontface": "*",
|
||||
"vee-validate": "^4.14.6",
|
||||
@@ -2752,6 +2753,14 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cytoscape": {
|
||||
"version": "3.34.1",
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.1.tgz",
|
||||
"integrity": "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@mdi/font": "7.4.47",
|
||||
"axios": "^1.13.5",
|
||||
"core-js": "^3.37.1",
|
||||
"cytoscape": "^3.34.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"roboto-fontface": "*",
|
||||
"vee-validate": "^4.14.6",
|
||||
|
||||
75
src/components/graph/CytoscapeChart.vue
Normal file
75
src/components/graph/CytoscapeChart.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="cytoscape-chart"
|
||||
:style="{ height: `${height}px` }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import cytoscape from 'cytoscape'
|
||||
|
||||
const props = defineProps({
|
||||
elements: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
styles: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
layout: {
|
||||
type: Object,
|
||||
default: () => ({ name: 'preset' }),
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
|
||||
const containerRef = ref(null)
|
||||
let cy = null
|
||||
|
||||
function runLayout () {
|
||||
cy.layout(props.layout).run()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
cy = cytoscape({
|
||||
container: containerRef.value,
|
||||
elements: props.elements,
|
||||
style: props.styles,
|
||||
layout: props.layout,
|
||||
})
|
||||
cy.on('tap', 'node', event => {
|
||||
emit('select', event.target.data())
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.elements,
|
||||
elements => {
|
||||
if (!cy) return
|
||||
cy.elements().remove()
|
||||
cy.add(elements)
|
||||
runLayout()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (cy) cy.destroy()
|
||||
cy = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cytoscape-chart {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
94
tests/unit/components/graph/CytoscapeChart.spec.js
Normal file
94
tests/unit/components/graph/CytoscapeChart.spec.js
Normal file
@@ -0,0 +1,94 @@
|
||||
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),
|
||||
}))
|
||||
|
||||
import cytoscape from 'cytoscape'
|
||||
|
||||
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))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user