Files
ComfyUI_frontend/src/components/rightSidePanel/parameters/TabSubgraphInputs.test.ts
Kelly Yang 22d32fee8c fix: address review feedback on TabSubgraphInputs drag-and-drop
- Fix stale index closures after reorder: watch promotionEntries and
  re-register drag handlers via nextTick so consecutive drags pass
  correct fromIndex/toIndex to movePromotion
- Replace `as number` type assertion with typeof guard per project guidelines
- Tighten cleanup test assertion to toHaveBeenCalledTimes(2) to verify
  both registered handlers are disposed on unmount
2026-04-20 21:12:22 -07:00

174 lines
4.6 KiB
TypeScript

import { createTestingPinia } from '@pinia/testing'
import { render } from '@testing-library/vue'
import { fromAny } from '@total-typescript/shoehorn'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { usePromotionStore } from '@/stores/promotionStore'
import TabSubgraphInputs from './TabSubgraphInputs.vue'
const {
mockDraggable,
mockDropTargetForElements,
capturedDropHandlers,
mockDraggableCleanup,
mockDropTargetCleanup
} = vi.hoisted(() => {
const capturedDropHandlers: Array<
(args: { source: { data: Record<string, unknown> } }) => void
> = []
const mockDraggableCleanup = vi.fn()
const mockDropTargetCleanup = vi.fn()
const mockDraggable = vi.fn(() => mockDraggableCleanup)
const mockDropTargetForElements = vi.fn(
(config: {
element: HTMLElement
onDrop: (args: { source: { data: Record<string, unknown> } }) => void
}) => {
capturedDropHandlers.push(config.onDrop)
return mockDropTargetCleanup
}
)
return {
mockDraggable,
mockDropTargetForElements,
capturedDropHandlers,
mockDraggableCleanup,
mockDropTargetCleanup
}
})
vi.mock('@atlaskit/pragmatic-drag-and-drop/element/adapter', () => ({
draggable: mockDraggable,
dropTargetForElements: mockDropTargetForElements
}))
vi.mock(
'@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue',
() => ({ default: { template: '<div />' } })
)
vi.mock('@/components/rightSidePanel/layout/CollapseToggleButton.vue', () => ({
default: { template: '<div />' }
}))
vi.mock('./SectionWidgets.vue', async () => {
const { defineComponent: dc, ref: r } = await import('vue')
return {
default: dc({
name: 'SectionWidgets',
props: [
'widgets',
'collapse',
'label',
'node',
'parents',
'isDraggable',
'enableEmptyState',
'tooltip',
'showNodeName'
],
emits: ['update:collapse'],
setup(
_: unknown,
{ expose }: { expose: (exposed: Record<string, unknown>) => void }
) {
const container = r<HTMLElement | undefined>(undefined)
expose({
widgetsContainer: container,
rootElement: r<HTMLElement | undefined>(undefined)
})
return { container }
},
template: `<div><div ref="container"><div class="draggable-item" /><div class="draggable-item" /></div></div>`
})
}
})
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
rightSidePanel: {
inputs: 'Inputs',
inputsNone: 'No Inputs',
inputsNoneTooltip: 'No inputs tooltip',
advancedInputs: 'Advanced Inputs',
noneSearchDesc: 'No results found'
}
}
}
})
function createMockNode(): SubgraphNode {
return fromAny<SubgraphNode, unknown>({
id: '1',
rootGraph: { id: 'graph-1' },
widgets: [],
subgraph: { nodes: [] }
})
}
function renderComponent(node: SubgraphNode = createMockNode()) {
return render(TabSubgraphInputs, {
props: { node },
global: { plugins: [i18n] }
})
}
describe('TabSubgraphInputs drag-and-drop', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
capturedDropHandlers.length = 0
vi.clearAllMocks()
})
it('attaches draggable and drop-target handlers to each item on mount', () => {
renderComponent()
expect(mockDraggable).toHaveBeenCalledTimes(2)
expect(mockDropTargetForElements).toHaveBeenCalledTimes(2)
})
it('runs cleanup for all registered handlers on unmount', () => {
const { unmount } = renderComponent()
unmount()
expect(mockDraggableCleanup).toHaveBeenCalledTimes(2)
expect(mockDropTargetCleanup).toHaveBeenCalledTimes(2)
})
it('calls movePromotion with correct indices when an item is dropped', () => {
const node = createMockNode()
renderComponent(node)
const promotionStore = usePromotionStore()
vi.spyOn(promotionStore, 'movePromotion')
capturedDropHandlers[1]({ source: { data: { index: 0 } } })
expect(promotionStore.movePromotion).toHaveBeenCalledWith(
'graph-1',
'1',
0,
1
)
})
it('does not call movePromotion when item is dropped onto itself', () => {
const node = createMockNode()
renderComponent(node)
const promotionStore = usePromotionStore()
vi.spyOn(promotionStore, 'movePromotion')
capturedDropHandlers[0]({ source: { data: { index: 0 } } })
expect(promotionStore.movePromotion).not.toHaveBeenCalled()
})
})