[test] Relocate and update test files

- Move TransformPane tests from .test.ts to .spec.ts format
- Relocate useWidgetValue tests to tests-ui directory
- Move QuadTree tests to tests-ui/tests/utils/spatial directory
- Add comprehensive tests for new composables:
  - useCanvasTransformSync tests
  - useTransformSettling tests
- Update test imports and paths

Tests now follow consistent organization patterns and cover the
refactored transform sync functionality.
This commit is contained in:
bymyself
2025-07-05 21:14:00 -07:00
parent 9a93764cc8
commit 4304bb3ca3
5 changed files with 519 additions and 4 deletions

View File

@@ -0,0 +1,239 @@
import type { LGraphCanvas } from '@comfyorg/litegraph'
import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { useCanvasTransformSync } from '@/composables/graph/useCanvasTransformSync'
// Mock LiteGraph canvas
const createMockCanvas = (): Partial<LGraphCanvas> => ({
canvas: document.createElement('canvas'),
ds: {
offset: [0, 0],
scale: 1
} as any // Mock the DragAndScale type
})
describe('useCanvasTransformSync', () => {
let mockCanvas: LGraphCanvas
let syncFn: ReturnType<typeof vi.fn>
let callbacks: {
onStart: ReturnType<typeof vi.fn>
onUpdate: ReturnType<typeof vi.fn>
onStop: ReturnType<typeof vi.fn>
}
beforeEach(() => {
vi.useFakeTimers()
mockCanvas = createMockCanvas() as LGraphCanvas
syncFn = vi.fn()
callbacks = {
onStart: vi.fn(),
onUpdate: vi.fn(),
onStop: vi.fn()
}
// Mock requestAnimationFrame
global.requestAnimationFrame = vi.fn((cb) => {
setTimeout(cb, 16) // Simulate 60fps
return 1
})
global.cancelAnimationFrame = vi.fn()
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
it('should auto-start sync when canvas is provided', async () => {
const { isActive } = useCanvasTransformSync(mockCanvas, syncFn, callbacks)
await nextTick()
expect(isActive.value).toBe(true)
expect(callbacks.onStart).toHaveBeenCalledOnce()
expect(syncFn).toHaveBeenCalledWith(mockCanvas)
})
it('should not auto-start when autoStart is false', async () => {
const { isActive } = useCanvasTransformSync(mockCanvas, syncFn, callbacks, {
autoStart: false
})
await nextTick()
expect(isActive.value).toBe(false)
expect(callbacks.onStart).not.toHaveBeenCalled()
expect(syncFn).not.toHaveBeenCalled()
})
it('should not start when canvas is null', async () => {
const { isActive } = useCanvasTransformSync(null, syncFn, callbacks)
await nextTick()
expect(isActive.value).toBe(false)
expect(callbacks.onStart).not.toHaveBeenCalled()
})
it('should manually start and stop sync', async () => {
const { isActive, startSync, stopSync } = useCanvasTransformSync(
mockCanvas,
syncFn,
callbacks,
{ autoStart: false }
)
// Start manually
startSync()
await nextTick()
expect(isActive.value).toBe(true)
expect(callbacks.onStart).toHaveBeenCalledOnce()
// Stop manually
stopSync()
await nextTick()
expect(isActive.value).toBe(false)
expect(callbacks.onStop).toHaveBeenCalledOnce()
})
it('should call sync function on each frame', async () => {
useCanvasTransformSync(mockCanvas, syncFn, callbacks)
await nextTick()
// Advance timers to trigger additional frames (initial call + 3 more = 4 total)
vi.advanceTimersByTime(48) // 3 additional frames at 16ms each
await nextTick()
expect(syncFn).toHaveBeenCalledTimes(4) // Initial call + 3 timed calls
expect(syncFn).toHaveBeenCalledWith(mockCanvas)
})
it('should provide timing information in onUpdate callback', async () => {
// Mock performance.now to return predictable values
const mockNow = vi.spyOn(performance, 'now')
mockNow.mockReturnValueOnce(0).mockReturnValueOnce(5) // 5ms duration
useCanvasTransformSync(mockCanvas, syncFn, callbacks)
await nextTick()
expect(callbacks.onUpdate).toHaveBeenCalledWith(5)
})
it('should handle sync function that throws errors', async () => {
const errorSyncFn = vi.fn().mockImplementation(() => {
throw new Error('Sync failed')
})
// Creating the composable should not throw
expect(() => {
useCanvasTransformSync(mockCanvas, errorSyncFn, callbacks)
}).not.toThrow()
await nextTick()
// Even though sync function throws, the composable should handle it gracefully
expect(errorSyncFn).toHaveBeenCalled()
expect(callbacks.onStart).toHaveBeenCalled()
})
it('should not start if already active', async () => {
const { startSync } = useCanvasTransformSync(mockCanvas, syncFn, callbacks)
await nextTick()
// Try to start again
startSync()
await nextTick()
// Should only be called once from auto-start
expect(callbacks.onStart).toHaveBeenCalledOnce()
})
it('should not stop if already inactive', async () => {
const { stopSync } = useCanvasTransformSync(mockCanvas, syncFn, callbacks, {
autoStart: false
})
// Try to stop when not started
stopSync()
await nextTick()
expect(callbacks.onStop).not.toHaveBeenCalled()
})
it('should clean up on component unmount', async () => {
const TestComponent = {
setup() {
const { isActive } = useCanvasTransformSync(
mockCanvas,
syncFn,
callbacks
)
return { isActive }
},
template: '<div>{{ isActive }}</div>'
}
const wrapper = mount(TestComponent)
await nextTick()
expect(callbacks.onStart).toHaveBeenCalled()
// Unmount component
wrapper.unmount()
await nextTick()
expect(callbacks.onStop).toHaveBeenCalled()
expect(global.cancelAnimationFrame).toHaveBeenCalled()
})
it('should work without callbacks', async () => {
const { isActive } = useCanvasTransformSync(mockCanvas, syncFn)
await nextTick()
expect(isActive.value).toBe(true)
expect(syncFn).toHaveBeenCalledWith(mockCanvas)
})
it('should stop sync when canvas becomes null during sync', async () => {
let currentCanvas: any = mockCanvas
const dynamicSyncFn = vi.fn(() => {
// Simulate canvas becoming null during sync
currentCanvas = null
})
const { isActive } = useCanvasTransformSync(
currentCanvas,
dynamicSyncFn,
callbacks
)
await nextTick()
expect(isActive.value).toBe(true)
// Advance time to trigger sync
vi.advanceTimersByTime(16)
await nextTick()
// Should handle null canvas gracefully
expect(dynamicSyncFn).toHaveBeenCalled()
})
it('should use cancelAnimationFrame when stopping', async () => {
const { stopSync } = useCanvasTransformSync(mockCanvas, syncFn, callbacks)
await nextTick()
stopSync()
expect(global.cancelAnimationFrame).toHaveBeenCalledWith(1)
})
})

View File

@@ -0,0 +1,277 @@
import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import { useTransformSettling } from '@/composables/graph/useTransformSettling'
describe('useTransformSettling', () => {
let element: HTMLDivElement
beforeEach(() => {
vi.useFakeTimers()
element = document.createElement('div')
document.body.appendChild(element)
})
afterEach(() => {
vi.useRealTimers()
document.body.removeChild(element)
})
it('should track wheel events and settle after delay', async () => {
const { isTransforming } = useTransformSettling(element)
// Initially not transforming
expect(isTransforming.value).toBe(false)
// Dispatch wheel event
element.dispatchEvent(new WheelEvent('wheel', { bubbles: true }))
await nextTick()
// Should be transforming
expect(isTransforming.value).toBe(true)
// Advance time but not past settle delay
vi.advanceTimersByTime(100)
expect(isTransforming.value).toBe(true)
// Advance past settle delay (default 200ms)
vi.advanceTimersByTime(150)
expect(isTransforming.value).toBe(false)
})
it('should reset settle timer on subsequent wheel events', async () => {
const { isTransforming } = useTransformSettling(element, {
settleDelay: 300
})
// First wheel event
element.dispatchEvent(new WheelEvent('wheel', { bubbles: true }))
await nextTick()
expect(isTransforming.value).toBe(true)
// Advance time partially
vi.advanceTimersByTime(200)
expect(isTransforming.value).toBe(true)
// Another wheel event should reset the timer
element.dispatchEvent(new WheelEvent('wheel', { bubbles: true }))
await nextTick()
// Advance 200ms more - should still be transforming
vi.advanceTimersByTime(200)
expect(isTransforming.value).toBe(true)
// Need another 100ms to settle (300ms total from last event)
vi.advanceTimersByTime(100)
expect(isTransforming.value).toBe(false)
})
it('should track pan events when trackPan is enabled', async () => {
const { isTransforming } = useTransformSettling(element, {
trackPan: true,
settleDelay: 200
})
// Pointer down should start transform
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
await nextTick()
expect(isTransforming.value).toBe(true)
// Pointer move should keep it active
vi.advanceTimersByTime(100)
element.dispatchEvent(new PointerEvent('pointermove', { bubbles: true }))
await nextTick()
// Should still be transforming
expect(isTransforming.value).toBe(true)
// Pointer up
element.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
await nextTick()
// Should still be transforming until settle delay
expect(isTransforming.value).toBe(true)
// Advance past settle delay
vi.advanceTimersByTime(200)
expect(isTransforming.value).toBe(false)
})
it('should not track pan events when trackPan is disabled', async () => {
const { isTransforming } = useTransformSettling(element, {
trackPan: false
})
// Pointer events should not trigger transform
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
element.dispatchEvent(new PointerEvent('pointermove', { bubbles: true }))
await nextTick()
expect(isTransforming.value).toBe(false)
})
it('should handle pointer cancel events', async () => {
const { isTransforming } = useTransformSettling(element, {
trackPan: true,
settleDelay: 200
})
// Start panning
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
await nextTick()
expect(isTransforming.value).toBe(true)
// Cancel instead of up
element.dispatchEvent(new PointerEvent('pointercancel', { bubbles: true }))
await nextTick()
// Should still settle normally
vi.advanceTimersByTime(200)
expect(isTransforming.value).toBe(false)
})
it('should work with ref target', async () => {
const targetRef = ref<HTMLElement | null>(null)
const { isTransforming } = useTransformSettling(targetRef)
// No target yet
expect(isTransforming.value).toBe(false)
// Set target
targetRef.value = element
await nextTick()
// Now events should work
element.dispatchEvent(new WheelEvent('wheel', { bubbles: true }))
await nextTick()
expect(isTransforming.value).toBe(true)
vi.advanceTimersByTime(200)
expect(isTransforming.value).toBe(false)
})
it('should use capture phase for events', async () => {
const captureHandler = vi.fn()
const bubbleHandler = vi.fn()
// Add handlers to verify capture phase
element.addEventListener('wheel', captureHandler, true)
element.addEventListener('wheel', bubbleHandler, false)
const { isTransforming } = useTransformSettling(element)
// Create child element
const child = document.createElement('div')
element.appendChild(child)
// Dispatch event on child
child.dispatchEvent(new WheelEvent('wheel', { bubbles: true }))
await nextTick()
// Capture handler should be called before bubble handler
expect(captureHandler).toHaveBeenCalled()
expect(isTransforming.value).toBe(true)
element.removeEventListener('wheel', captureHandler, true)
element.removeEventListener('wheel', bubbleHandler, false)
})
it('should throttle pointer move events', async () => {
const { isTransforming } = useTransformSettling(element, {
trackPan: true,
pointerMoveThrottle: 50,
settleDelay: 100
})
// Start panning
element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
await nextTick()
// Fire many pointer move events rapidly
for (let i = 0; i < 10; i++) {
element.dispatchEvent(new PointerEvent('pointermove', { bubbles: true }))
vi.advanceTimersByTime(5) // 5ms between events
}
await nextTick()
// Should still be transforming
expect(isTransforming.value).toBe(true)
// End panning
element.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }))
// Advance past settle delay
vi.advanceTimersByTime(100)
expect(isTransforming.value).toBe(false)
})
it('should clean up event listeners when component unmounts', async () => {
const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener')
// Create a test component
const TestComponent = {
setup() {
const { isTransforming } = useTransformSettling(element, {
trackPan: true
})
return { isTransforming }
},
template: '<div>{{ isTransforming }}</div>'
}
const wrapper = mount(TestComponent)
await nextTick()
// Unmount component
wrapper.unmount()
// Should have removed all event listeners
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'wheel',
expect.any(Function),
expect.objectContaining({ capture: true })
)
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'pointerdown',
expect.any(Function),
expect.objectContaining({ capture: true })
)
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'pointermove',
expect.any(Function),
expect.objectContaining({ capture: true })
)
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'pointerup',
expect.any(Function),
expect.objectContaining({ capture: true })
)
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'pointercancel',
expect.any(Function),
expect.objectContaining({ capture: true })
)
})
it('should use passive listeners when specified', async () => {
const addEventListenerSpy = vi.spyOn(element, 'addEventListener')
useTransformSettling(element, {
passive: true,
trackPan: true
})
// Check that passive option was used for appropriate events
expect(addEventListenerSpy).toHaveBeenCalledWith(
'wheel',
expect.any(Function),
expect.objectContaining({ passive: true, capture: true })
)
expect(addEventListenerSpy).toHaveBeenCalledWith(
'pointermove',
expect.any(Function),
expect.objectContaining({ passive: true, capture: true })
)
})
})

View File

@@ -9,14 +9,13 @@ import {
} from 'vitest'
import { ref } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
useBooleanWidgetValue,
useNumberWidgetValue,
useStringWidgetValue,
useWidgetValue
} from './useWidgetValue'
} from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
describe('useWidgetValue', () => {
let mockWidget: SimplifiedWidget<string>

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { type Bounds, QuadTree } from './QuadTree'
import { type Bounds, QuadTree } from '@/utils/spatial/QuadTree'
describe('QuadTree', () => {
let quadTree: QuadTree<string>