mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
*PR Created by the Glary-Bot Agent* --- ## Summary - Replace all `as unknown as Type` assertions in 59 unit test files with type-safe `@total-typescript/shoehorn` functions - Use `fromPartial<Type>()` for partial mock objects where deep-partial type-checks (21 files) - Use `fromAny<Type>()` for fundamentally incompatible types: null, undefined, primitives, variables, class expressions, and mocks with test-specific extra properties that `PartialDeepObject` rejects (remaining files) - All explicit type parameters preserved so TypeScript return types are correct - Browser test `.spec.ts` files excluded (shoehorn unavailable in `page.evaluate` browser context) ## Verification - `pnpm typecheck` ✅ - `pnpm lint` ✅ - `pnpm format` ✅ - Pre-commit hooks passed (format + oxlint + eslint + typecheck) - Migrated test files verified passing (ran representative subset) - No test behavior changes — only type assertion syntax changed - No UI changes — screenshots not applicable ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10761-test-migrate-as-unknown-as-to-total-typescript-shoehorn-3336d73d365081f6b8adc44db5dcc380) by [Unito](https://www.unito.io) --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com> Co-authored-by: Amp <amp@ampcode.com>
346 lines
11 KiB
TypeScript
346 lines
11 KiB
TypeScript
import { fromPartial } from '@total-typescript/shoehorn'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ref } from 'vue'
|
|
import type { Ref } from 'vue'
|
|
|
|
import type { NodeLayout } from '@/renderer/core/layout/types'
|
|
|
|
// TODO: Simplify test setup — use real layoutStore + createTestingPinia instead
|
|
// of manually mocking every dependency. See https://github.com/Comfy-Org/ComfyUI_frontend/issues/10765
|
|
const testState = vi.hoisted(() => {
|
|
// Imports are unavailable inside vi.hoisted() so shoehorn's fromAny cannot
|
|
// be used here. This local identity function serves the same purpose
|
|
// (runtime no-op cast) until the test is rewritten to use real stores.
|
|
const placeholder = <T>(v: unknown): T => v as T
|
|
return {
|
|
selectedNodeIds: placeholder<Ref<Set<string>>>(null),
|
|
selectedItems: placeholder<Ref<unknown[]>>(null),
|
|
nodeLayouts: new Map<string, Pick<NodeLayout, 'position' | 'size'>>(),
|
|
mutationFns: {
|
|
setSource: vi.fn(),
|
|
moveNode: vi.fn(),
|
|
batchMoveNodes: vi.fn()
|
|
},
|
|
batchUpdateNodeBounds: vi.fn(),
|
|
nodeSnap: {
|
|
shouldSnap: vi.fn(() => false),
|
|
applySnapToPosition: vi.fn((pos: { x: number; y: number }) => pos)
|
|
},
|
|
cancelAnimationFrame: vi.fn(),
|
|
requestAnimationFrameCallback: null as FrameRequestCallback | null,
|
|
capturedOnPan: {
|
|
current: null as ((dx: number, dy: number) => void) | null
|
|
},
|
|
capturedAutoPanInstance: {
|
|
current: null as {
|
|
updatePointer: ReturnType<typeof vi.fn>
|
|
start: ReturnType<typeof vi.fn>
|
|
stop: ReturnType<typeof vi.fn>
|
|
} | null
|
|
},
|
|
mockDs: { offset: [0, 0] as [number, number], scale: 1 }
|
|
}
|
|
})
|
|
|
|
vi.mock('pinia', () => ({
|
|
storeToRefs: <T>(store: T) => store
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/canvas/useAutoPan', () => ({
|
|
AutoPanController: class {
|
|
updatePointer = vi.fn()
|
|
start = vi.fn()
|
|
stop = vi.fn()
|
|
constructor(opts: { onPan: (dx: number, dy: number) => void }) {
|
|
testState.capturedOnPan.current = opts.onPan
|
|
testState.capturedAutoPanInstance.current = this
|
|
}
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
|
useCanvasStore: () => ({
|
|
selectedNodeIds: testState.selectedNodeIds,
|
|
selectedItems: testState.selectedItems,
|
|
canvas: {
|
|
ds: testState.mockDs,
|
|
auto_pan_speed: 10,
|
|
canvas: {
|
|
getBoundingClientRect: () => ({
|
|
left: 0,
|
|
top: 0,
|
|
right: 800,
|
|
bottom: 600
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/layout/operations/layoutMutations', () => ({
|
|
useLayoutMutations: () => testState.mutationFns
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
|
|
layoutStore: {
|
|
getNodeLayoutRef: (nodeId: string) =>
|
|
ref(testState.nodeLayouts.get(nodeId) ?? null),
|
|
batchUpdateNodeBounds: testState.batchUpdateNodeBounds
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/renderer/extensions/vueNodes/composables/useNodeSnap', () => ({
|
|
useNodeSnap: () => testState.nodeSnap
|
|
}))
|
|
|
|
vi.mock('@/renderer/extensions/vueNodes/composables/useShiftKeySync', () => ({
|
|
useShiftKeySync: () => ({
|
|
trackShiftKey: () => () => {}
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/renderer/core/layout/transform/useTransformState', () => ({
|
|
useTransformState: () => ({
|
|
screenToCanvas: ({ x, y }: { x: number; y: number }) => ({
|
|
x: x / (testState.mockDs.scale || 1) - testState.mockDs.offset[0],
|
|
y: y / (testState.mockDs.scale || 1) - testState.mockDs.offset[1]
|
|
})
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/utils/litegraphUtil', () => ({
|
|
isLGraphGroup: () => false
|
|
}))
|
|
|
|
vi.mock('@vueuse/core', () => ({
|
|
createSharedComposable: (fn: () => unknown) => fn
|
|
}))
|
|
|
|
import { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'
|
|
|
|
function pointerEvent(clientX: number, clientY: number): PointerEvent {
|
|
const target = document.createElement('div')
|
|
target.hasPointerCapture = vi.fn(() => false)
|
|
target.setPointerCapture = vi.fn()
|
|
return fromPartial<PointerEvent>({ clientX, clientY, target, pointerId: 1 })
|
|
}
|
|
|
|
describe('useNodeDrag', () => {
|
|
beforeEach(() => {
|
|
testState.selectedNodeIds = ref(new Set<string>())
|
|
testState.selectedItems = ref<unknown[]>([])
|
|
testState.nodeLayouts.clear()
|
|
testState.mutationFns.setSource.mockReset()
|
|
testState.mutationFns.moveNode.mockReset()
|
|
testState.mutationFns.batchMoveNodes.mockReset()
|
|
testState.batchUpdateNodeBounds.mockReset()
|
|
testState.nodeSnap.shouldSnap.mockReset()
|
|
testState.nodeSnap.shouldSnap.mockReturnValue(false)
|
|
testState.nodeSnap.applySnapToPosition.mockReset()
|
|
testState.nodeSnap.applySnapToPosition.mockImplementation(
|
|
(pos: { x: number; y: number }) => pos
|
|
)
|
|
testState.cancelAnimationFrame.mockReset()
|
|
testState.requestAnimationFrameCallback = null
|
|
testState.capturedOnPan.current = null
|
|
testState.capturedAutoPanInstance.current = null
|
|
testState.mockDs.offset = [0, 0]
|
|
testState.mockDs.scale = 1
|
|
|
|
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
|
testState.requestAnimationFrameCallback = cb
|
|
return 1
|
|
})
|
|
vi.stubGlobal('cancelAnimationFrame', testState.cancelAnimationFrame)
|
|
})
|
|
|
|
it('batches multi-node drag updates into one mutation call per frame', () => {
|
|
testState.selectedNodeIds.value = new Set(['1', '2'])
|
|
testState.nodeLayouts.set('1', {
|
|
position: { x: 100, y: 100 },
|
|
size: { width: 200, height: 120 }
|
|
})
|
|
testState.nodeLayouts.set('2', {
|
|
position: { x: 200, y: 180 },
|
|
size: { width: 210, height: 130 }
|
|
})
|
|
|
|
const { startDrag, handleDrag } = useNodeDrag()
|
|
|
|
startDrag(pointerEvent(10, 20), '1')
|
|
handleDrag(pointerEvent(30, 40), '1')
|
|
testState.requestAnimationFrameCallback?.(0)
|
|
|
|
expect(testState.mutationFns.batchMoveNodes).toHaveBeenCalledTimes(1)
|
|
expect(testState.mutationFns.batchMoveNodes).toHaveBeenCalledWith([
|
|
{ nodeId: '1', position: { x: 120, y: 120 } },
|
|
{ nodeId: '2', position: { x: 220, y: 200 } }
|
|
])
|
|
expect(testState.mutationFns.moveNode).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('uses the same batched mutation path for single-node drags', () => {
|
|
testState.selectedNodeIds.value = new Set(['1'])
|
|
testState.nodeLayouts.set('1', {
|
|
position: { x: 50, y: 80 },
|
|
size: { width: 180, height: 110 }
|
|
})
|
|
|
|
const { startDrag, handleDrag } = useNodeDrag()
|
|
|
|
startDrag(pointerEvent(5, 10), '1')
|
|
handleDrag(pointerEvent(25, 30), '1')
|
|
testState.requestAnimationFrameCallback?.(0)
|
|
|
|
expect(testState.mutationFns.batchMoveNodes).toHaveBeenCalledTimes(1)
|
|
expect(testState.mutationFns.batchMoveNodes).toHaveBeenCalledWith([
|
|
{ nodeId: '1', position: { x: 70, y: 100 } }
|
|
])
|
|
expect(testState.mutationFns.moveNode).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('cancels pending RAF and applies snap updates on endDrag', () => {
|
|
testState.selectedNodeIds.value = new Set(['1'])
|
|
testState.nodeLayouts.set('1', {
|
|
position: { x: 50, y: 80 },
|
|
size: { width: 180, height: 110 }
|
|
})
|
|
testState.nodeSnap.shouldSnap.mockReturnValue(true)
|
|
testState.nodeSnap.applySnapToPosition.mockImplementation(({ x, y }) => ({
|
|
x: x + 5,
|
|
y: y + 7
|
|
}))
|
|
|
|
const { startDrag, handleDrag, endDrag } = useNodeDrag()
|
|
|
|
startDrag(pointerEvent(5, 10), '1')
|
|
handleDrag(pointerEvent(25, 30), '1')
|
|
endDrag({} as PointerEvent, '1')
|
|
|
|
expect(testState.cancelAnimationFrame).toHaveBeenCalledTimes(1)
|
|
expect(testState.cancelAnimationFrame).toHaveBeenCalledWith(1)
|
|
expect(testState.batchUpdateNodeBounds).toHaveBeenCalledTimes(1)
|
|
expect(testState.batchUpdateNodeBounds).toHaveBeenCalledWith([
|
|
{
|
|
nodeId: '1',
|
|
bounds: {
|
|
x: 55,
|
|
y: 87,
|
|
width: 180,
|
|
height: 110
|
|
}
|
|
}
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('useNodeDrag auto-pan', () => {
|
|
beforeEach(() => {
|
|
testState.selectedNodeIds = ref(new Set(['1']))
|
|
testState.selectedItems = ref<unknown[]>([])
|
|
testState.nodeLayouts.clear()
|
|
testState.nodeLayouts.set('1', {
|
|
position: { x: 100, y: 200 },
|
|
size: { width: 200, height: 100 }
|
|
})
|
|
testState.nodeLayouts.set('2', {
|
|
position: { x: 300, y: 400 },
|
|
size: { width: 200, height: 100 }
|
|
})
|
|
testState.mutationFns.setSource.mockReset()
|
|
testState.mutationFns.moveNode.mockReset()
|
|
testState.mutationFns.batchMoveNodes.mockReset()
|
|
testState.batchUpdateNodeBounds.mockReset()
|
|
testState.nodeSnap.shouldSnap.mockReset()
|
|
testState.nodeSnap.shouldSnap.mockReturnValue(false)
|
|
testState.nodeSnap.applySnapToPosition.mockReset()
|
|
testState.nodeSnap.applySnapToPosition.mockImplementation(
|
|
(pos: { x: number; y: number }) => pos
|
|
)
|
|
testState.cancelAnimationFrame.mockReset()
|
|
testState.requestAnimationFrameCallback = null
|
|
testState.capturedOnPan.current = null
|
|
testState.capturedAutoPanInstance.current = null
|
|
testState.mockDs.offset = [0, 0]
|
|
testState.mockDs.scale = 1
|
|
|
|
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
|
testState.requestAnimationFrameCallback = cb
|
|
return 1
|
|
})
|
|
vi.stubGlobal('cancelAnimationFrame', testState.cancelAnimationFrame)
|
|
})
|
|
|
|
it('moves node when auto-pan shifts the canvas offset', () => {
|
|
const drag = useNodeDrag()
|
|
drag.startDrag(pointerEvent(750, 300), '1')
|
|
|
|
drag.handleDrag(pointerEvent(760, 300), '1')
|
|
testState.requestAnimationFrameCallback?.(0)
|
|
|
|
expect(testState.mutationFns.batchMoveNodes).toHaveBeenLastCalledWith([
|
|
{ nodeId: '1', position: { x: 110, y: 200 } }
|
|
])
|
|
|
|
testState.mutationFns.batchMoveNodes.mockClear()
|
|
|
|
testState.mockDs.offset[0] -= 5
|
|
testState.capturedOnPan.current!(5, 0)
|
|
|
|
expect(testState.mutationFns.batchMoveNodes).toHaveBeenCalledWith([
|
|
{ nodeId: '1', position: { x: 115, y: 200 } }
|
|
])
|
|
})
|
|
|
|
it('moves all selected nodes when auto-pan fires', () => {
|
|
testState.selectedNodeIds.value = new Set(['1', '2'])
|
|
const drag = useNodeDrag()
|
|
|
|
drag.startDrag(pointerEvent(750, 300), '1')
|
|
testState.mutationFns.batchMoveNodes.mockClear()
|
|
|
|
testState.mockDs.offset[0] -= 5
|
|
testState.capturedOnPan.current!(5, 0)
|
|
|
|
expect(testState.mutationFns.batchMoveNodes).toHaveBeenCalledTimes(1)
|
|
const calls = testState.mutationFns.batchMoveNodes.mock.calls[0][0]
|
|
const nodeIds = calls.map((u: { nodeId: string }) => u.nodeId)
|
|
expect(nodeIds).toContain('1')
|
|
expect(nodeIds).toContain('2')
|
|
})
|
|
|
|
it('updates auto-pan pointer on handleDrag', () => {
|
|
const drag = useNodeDrag()
|
|
drag.startDrag(pointerEvent(400, 300), '1')
|
|
|
|
drag.handleDrag(pointerEvent(790, 300), '1')
|
|
|
|
expect(
|
|
testState.capturedAutoPanInstance.current!.updatePointer
|
|
).toHaveBeenCalledWith(790, 300)
|
|
})
|
|
|
|
it('stops auto-pan on endDrag', () => {
|
|
const drag = useNodeDrag()
|
|
drag.startDrag(pointerEvent(400, 300), '1')
|
|
expect(testState.capturedAutoPanInstance.current).not.toBeNull()
|
|
|
|
drag.endDrag(pointerEvent(400, 300), '1')
|
|
|
|
expect(testState.capturedAutoPanInstance.current!.stop).toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not move nodes if onPan fires after endDrag', () => {
|
|
const drag = useNodeDrag()
|
|
drag.startDrag(pointerEvent(400, 300), '1')
|
|
const onPan = testState.capturedOnPan.current!
|
|
|
|
drag.endDrag(pointerEvent(400, 300), '1')
|
|
testState.mutationFns.batchMoveNodes.mockClear()
|
|
|
|
onPan(5, 0)
|
|
|
|
expect(testState.mutationFns.batchMoveNodes).not.toHaveBeenCalled()
|
|
})
|
|
})
|