diff --git a/package-lock.json b/package-lock.json index 2312abe..f5a22c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8818240..a6a6774 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/graph/CytoscapeChart.vue b/src/components/graph/CytoscapeChart.vue new file mode 100644 index 0000000..ced6cc4 --- /dev/null +++ b/src/components/graph/CytoscapeChart.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/tests/unit/components/graph/CytoscapeChart.spec.js b/tests/unit/components/graph/CytoscapeChart.spec.js new file mode 100644 index 0000000..81cca6e --- /dev/null +++ b/tests/unit/components/graph/CytoscapeChart.spec.js @@ -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)) + }) +})