mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Merge branch 'main' into queue-overlay-deletions
This commit is contained in:
171
tests-ui/tests/composables/graph/useTransformSettling.test.ts
Normal file
171
tests-ui/tests/composables/graph/useTransformSettling.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useTransformSettling } from '@/renderer/core/layout/transform/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, {
|
||||
settleDelay: 200
|
||||
})
|
||||
|
||||
// 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
|
||||
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 not track pan events', async () => {
|
||||
const { isTransforming } = useTransformSettling(element)
|
||||
|
||||
// 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 work with ref target', async () => {
|
||||
const targetRef = ref<HTMLElement | null>(null)
|
||||
const { isTransforming } = useTransformSettling(targetRef, {
|
||||
settleDelay: 200
|
||||
})
|
||||
|
||||
// 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 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)
|
||||
return { isTransforming }
|
||||
},
|
||||
template: '<div>{{ isTransforming }}</div>'
|
||||
}
|
||||
|
||||
const wrapper = mount(TestComponent)
|
||||
await nextTick()
|
||||
|
||||
// Unmount component
|
||||
wrapper.unmount()
|
||||
|
||||
// Should have removed wheel event listener
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
||||
'wheel',
|
||||
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
|
||||
})
|
||||
|
||||
// Check that passive option was used for wheel event
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
||||
'wheel',
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ passive: true, capture: true })
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -577,32 +577,32 @@ describe('useNodePricing', () => {
|
||||
{
|
||||
rendering_speed: 'Quality',
|
||||
character_image: false,
|
||||
expected: '$0.09/Run'
|
||||
expected: '$0.13/Run'
|
||||
},
|
||||
{
|
||||
rendering_speed: 'Quality',
|
||||
character_image: true,
|
||||
expected: '$0.20/Run'
|
||||
expected: '$0.29/Run'
|
||||
},
|
||||
{
|
||||
rendering_speed: 'Default',
|
||||
character_image: false,
|
||||
expected: '$0.06/Run'
|
||||
expected: '$0.09/Run'
|
||||
},
|
||||
{
|
||||
rendering_speed: 'Default',
|
||||
character_image: true,
|
||||
expected: '$0.15/Run'
|
||||
expected: '$0.21/Run'
|
||||
},
|
||||
{
|
||||
rendering_speed: 'Turbo',
|
||||
character_image: false,
|
||||
expected: '$0.03/Run'
|
||||
expected: '$0.04/Run'
|
||||
},
|
||||
{
|
||||
rendering_speed: 'Turbo',
|
||||
character_image: true,
|
||||
expected: '$0.10/Run'
|
||||
expected: '$0.14/Run'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -623,7 +623,7 @@ describe('useNodePricing', () => {
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe(
|
||||
'$0.03-0.08 x num_images/Run (varies with rendering speed & num_images)'
|
||||
'$0.04-0.11 x num_images/Run (varies with rendering speed & num_images)'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -635,7 +635,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.27/Run') // 0.09 * 3
|
||||
expect(price).toBe('$0.39/Run') // 0.09 * 3 * 1.43
|
||||
})
|
||||
|
||||
it('should multiply price by num_images for Turbo rendering speed', () => {
|
||||
@@ -646,7 +646,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.15/Run') // 0.03 * 5
|
||||
expect(price).toBe('$0.21/Run') // 0.03 * 5 * 1.43
|
||||
})
|
||||
})
|
||||
|
||||
@@ -770,7 +770,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$2.19/Run')
|
||||
expect(price).toBe('$3.13/Run')
|
||||
})
|
||||
|
||||
it('should return $6.37 for ray-2 4K 5s', () => {
|
||||
@@ -782,7 +782,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$6.37/Run')
|
||||
expect(price).toBe('$9.11/Run')
|
||||
})
|
||||
|
||||
it('should return $0.35 for ray-1-6 model', () => {
|
||||
@@ -794,7 +794,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.35/Run')
|
||||
expect(price).toBe('$0.50/Run')
|
||||
})
|
||||
|
||||
it('should return range when widgets are missing', () => {
|
||||
@@ -803,7 +803,7 @@ describe('useNodePricing', () => {
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe(
|
||||
'$0.14-11.47/Run (varies with model, resolution & duration)'
|
||||
'$0.20-16.40/Run (varies with model, resolution & duration)'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1192,7 +1192,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.18/Run') // 0.06 * 3
|
||||
expect(price).toBe('$0.26/Run') // 0.06 * 3 * 1.43
|
||||
})
|
||||
|
||||
it('should calculate dynamic pricing for IdeogramV2 based on num_images value', () => {
|
||||
@@ -1202,7 +1202,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.32/Run') // 0.08 * 4
|
||||
expect(price).toBe('$0.46/Run') // 0.08 * 4 * 1.43
|
||||
})
|
||||
|
||||
it('should fall back to static display when num_images widget is missing for IdeogramV1', () => {
|
||||
@@ -1210,7 +1210,7 @@ describe('useNodePricing', () => {
|
||||
const node = createMockNode('IdeogramV1', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.02-0.06 x num_images/Run')
|
||||
expect(price).toBe('$0.03-0.09 x num_images/Run')
|
||||
})
|
||||
|
||||
it('should fall back to static display when num_images widget is missing for IdeogramV2', () => {
|
||||
@@ -1218,7 +1218,7 @@ describe('useNodePricing', () => {
|
||||
const node = createMockNode('IdeogramV2', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05-0.08 x num_images/Run')
|
||||
expect(price).toBe('$0.07-0.11 x num_images/Run')
|
||||
})
|
||||
|
||||
it('should handle edge case when num_images value is 1 for IdeogramV1', () => {
|
||||
@@ -1228,7 +1228,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.06/Run') // 0.06 * 1 (turbo=false by default)
|
||||
expect(price).toBe('$0.09/Run') // 0.06 * 1 * 1.43 (turbo=false by default)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1435,7 +1435,7 @@ describe('useNodePricing', () => {
|
||||
const node = createMockNode('RunwayTextToImageNode')
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.08/Run')
|
||||
expect(price).toBe('$0.11/Run')
|
||||
})
|
||||
|
||||
it('should calculate dynamic pricing for RunwayImageToVideoNodeGen3a', () => {
|
||||
@@ -1445,7 +1445,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.50/Run') // 0.05 * 10
|
||||
expect(price).toBe('$0.71/Run') // 0.05 * 10 * 1.43
|
||||
})
|
||||
|
||||
it('should return fallback for RunwayImageToVideoNodeGen3a without duration', () => {
|
||||
@@ -1453,7 +1453,7 @@ describe('useNodePricing', () => {
|
||||
const node = createMockNode('RunwayImageToVideoNodeGen3a', [])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.05/second')
|
||||
expect(price).toBe('$0.0715/second')
|
||||
})
|
||||
|
||||
it('should handle zero duration for RunwayImageToVideoNodeGen3a', () => {
|
||||
@@ -1473,7 +1473,7 @@ describe('useNodePricing', () => {
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$0.25/Run') // Falls back to 5 seconds: 0.05 * 5
|
||||
expect(price).toBe('$0.36/Run') // Falls back to 5 seconds: 0.05 * 5 * 1.43
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1810,8 +1810,8 @@ describe('useNodePricing', () => {
|
||||
// Test edge cases
|
||||
const testCases = [
|
||||
{ duration: 0, expected: '$0.00/Run' }, // Now correctly handles 0 duration
|
||||
{ duration: 1, expected: '$0.05/Run' },
|
||||
{ duration: 30, expected: '$1.50/Run' }
|
||||
{ duration: 1, expected: '$0.07/Run' },
|
||||
{ duration: 30, expected: '$2.15/Run' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ duration, expected }) => {
|
||||
@@ -1828,7 +1828,7 @@ describe('useNodePricing', () => {
|
||||
{ name: 'duration', value: 'invalid-string' }
|
||||
])
|
||||
// When Number('invalid-string') returns NaN, it falls back to 5 seconds
|
||||
expect(getNodeDisplayPrice(node)).toBe('$0.25/Run')
|
||||
expect(getNodeDisplayPrice(node)).toBe('$0.36/Run')
|
||||
})
|
||||
|
||||
it('should handle missing duration widget gracefully', () => {
|
||||
@@ -1841,7 +1841,7 @@ describe('useNodePricing', () => {
|
||||
|
||||
nodes.forEach((nodeType) => {
|
||||
const node = createMockNode(nodeType, [])
|
||||
expect(getNodeDisplayPrice(node)).toBe('$0.05/second')
|
||||
expect(getNodeDisplayPrice(node)).toBe('$0.0715/second')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@ vi.mock('@/scripts/app', () => {
|
||||
}
|
||||
}),
|
||||
canvas: mockCanvas,
|
||||
graph: {
|
||||
rootGraph: {
|
||||
clear: mockGraphClear
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ describe('useCoreCommands', () => {
|
||||
await clearCommand.function()
|
||||
|
||||
expect(app.clean).toHaveBeenCalled()
|
||||
expect(app.graph.clear).toHaveBeenCalled()
|
||||
expect(app.rootGraph.clear).toHaveBeenCalled()
|
||||
expect(api.dispatchCustomEvent).toHaveBeenCalledWith('graphCleared')
|
||||
})
|
||||
|
||||
@@ -178,7 +178,7 @@ describe('useCoreCommands', () => {
|
||||
await clearCommand.function()
|
||||
|
||||
expect(app.clean).toHaveBeenCalled()
|
||||
expect(app.graph.clear).not.toHaveBeenCalled()
|
||||
expect(app.rootGraph.clear).not.toHaveBeenCalled()
|
||||
|
||||
// Should only remove user nodes, not input/output nodes
|
||||
expect(mockSubgraph.remove).toHaveBeenCalledTimes(2)
|
||||
@@ -212,7 +212,7 @@ describe('useCoreCommands', () => {
|
||||
|
||||
// Should not clear anything when user cancels
|
||||
expect(app.clean).not.toHaveBeenCalled()
|
||||
expect(app.graph.clear).not.toHaveBeenCalled()
|
||||
expect(app.rootGraph.clear).not.toHaveBeenCalled()
|
||||
expect(api.dispatchCustomEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { LGraphNode, LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
@@ -40,12 +39,10 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
const mockApp: { rootGraph?: Partial<LGraph> } = vi.hoisted(() => ({}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {
|
||||
nodes: []
|
||||
}
|
||||
}
|
||||
app: mockApp
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
@@ -107,9 +104,8 @@ describe('useMissingNodes', () => {
|
||||
nodeDefsByName: {}
|
||||
})
|
||||
|
||||
// Reset app.graph.nodes
|
||||
// @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing.
|
||||
app.graph.nodes = []
|
||||
// Reset app.rootGraph.nodes
|
||||
mockApp.rootGraph = { nodes: [] }
|
||||
|
||||
// Default mock for collectAllNodes - returns empty array
|
||||
mockCollectAllNodes.mockReturnValue([])
|
||||
@@ -306,13 +302,7 @@ describe('useMissingNodes', () => {
|
||||
})
|
||||
|
||||
describe('missing core nodes detection', () => {
|
||||
const createMockNode = (
|
||||
type: string,
|
||||
packId?: string,
|
||||
version?: string
|
||||
): LGraphNode =>
|
||||
// @ts-expect-error - Creating a partial mock of LGraphNode for testing.
|
||||
// We only need specific properties for our tests, not the full LGraphNode interface.
|
||||
const createMockNode = (type: string, packId?: string, version?: string) =>
|
||||
({
|
||||
type,
|
||||
properties: { cnr_id: packId, ver: version },
|
||||
@@ -325,7 +315,7 @@ describe('useMissingNodes', () => {
|
||||
mode: 0,
|
||||
inputs: [],
|
||||
outputs: []
|
||||
})
|
||||
}) as unknown as LGraphNode
|
||||
|
||||
it('identifies missing core nodes not in nodeDefStore', () => {
|
||||
const coreNode1 = createMockNode('CoreNode1', 'comfy-core', '1.2.0')
|
||||
@@ -467,8 +457,7 @@ describe('useMissingNodes', () => {
|
||||
|
||||
it('calls collectAllNodes with the app graph and filter function', () => {
|
||||
const mockGraph = { nodes: [], subgraphs: new Map() }
|
||||
// @ts-expect-error - Mocking app.graph for testing
|
||||
app.graph = mockGraph
|
||||
mockApp.rootGraph = mockGraph
|
||||
|
||||
const { missingCoreNodes } = useMissingNodes()
|
||||
// Access the computed to trigger the function
|
||||
@@ -490,8 +479,7 @@ describe('useMissingNodes', () => {
|
||||
|
||||
it('filter function correctly identifies missing core nodes', () => {
|
||||
const mockGraph = { nodes: [], subgraphs: new Map() }
|
||||
// @ts-expect-error - Mocking app.graph for testing
|
||||
app.graph = mockGraph
|
||||
mockApp.rootGraph = mockGraph
|
||||
|
||||
mockUseNodeDefStore.mockReturnValue({
|
||||
nodeDefsByName: {
|
||||
@@ -579,14 +567,13 @@ describe('useMissingNodes', () => {
|
||||
subgraph: mockSubgraph,
|
||||
type: 'SubgraphContainer',
|
||||
properties: { cnr_id: 'custom-pack' }
|
||||
}
|
||||
} as unknown as LGraphNode
|
||||
|
||||
const mockMainGraph = {
|
||||
nodes: [mainMissingNode, mockSubgraphNode]
|
||||
}
|
||||
} as Partial<LGraph> as LGraph
|
||||
|
||||
// @ts-expect-error - Mocking app.graph for testing
|
||||
app.graph = mockMainGraph
|
||||
mockApp.rootGraph = mockMainGraph
|
||||
|
||||
mockUseNodeDefStore.mockReturnValue({
|
||||
nodeDefsByName: {
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
import StripePricingTable from '@/platform/cloud/subscription/components/StripePricingTable.vue'
|
||||
|
||||
const mockLoadStripeScript = vi.fn()
|
||||
let currentConfig = {
|
||||
publishableKey: 'pk_test_123',
|
||||
pricingTableId: 'prctbl_123'
|
||||
}
|
||||
let hasConfig = true
|
||||
|
||||
vi.mock('@/config/stripePricingTableConfig', () => ({
|
||||
getStripePricingTableConfig: () => currentConfig,
|
||||
hasStripePricingTableConfig: () => hasConfig
|
||||
}))
|
||||
|
||||
const mockIsLoaded = ref(false)
|
||||
const mockIsLoading = ref(false)
|
||||
const mockError = ref(null)
|
||||
|
||||
vi.mock(
|
||||
'@/platform/cloud/subscription/composables/useStripePricingTableLoader',
|
||||
() => ({
|
||||
useStripePricingTableLoader: () => ({
|
||||
loadScript: mockLoadStripeScript,
|
||||
isLoaded: mockIsLoaded,
|
||||
isLoading: mockIsLoading,
|
||||
error: mockError
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
const mountComponent = () =>
|
||||
mount(StripePricingTable, {
|
||||
global: {
|
||||
plugins: [i18n]
|
||||
}
|
||||
})
|
||||
|
||||
describe('StripePricingTable', () => {
|
||||
beforeEach(() => {
|
||||
currentConfig = {
|
||||
publishableKey: 'pk_test_123',
|
||||
pricingTableId: 'prctbl_123'
|
||||
}
|
||||
hasConfig = true
|
||||
mockLoadStripeScript.mockReset().mockResolvedValue(undefined)
|
||||
mockIsLoaded.value = false
|
||||
mockIsLoading.value = false
|
||||
mockError.value = null
|
||||
})
|
||||
|
||||
it('renders the Stripe pricing table when config is available', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(mockLoadStripeScript).toHaveBeenCalled()
|
||||
|
||||
const stripePricingTable = wrapper.find('stripe-pricing-table')
|
||||
expect(stripePricingTable.exists()).toBe(true)
|
||||
expect(stripePricingTable.attributes('publishable-key')).toBe('pk_test_123')
|
||||
expect(stripePricingTable.attributes('pricing-table-id')).toBe('prctbl_123')
|
||||
})
|
||||
|
||||
it('shows missing config message when credentials are absent', () => {
|
||||
hasConfig = false
|
||||
currentConfig = { publishableKey: '', pricingTableId: '' }
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-testid="stripe-table-missing-config"]').exists()
|
||||
).toBe(true)
|
||||
expect(mockLoadStripeScript).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows loading indicator when script is loading', async () => {
|
||||
// Mock loadScript to never resolve, simulating loading state
|
||||
mockLoadStripeScript.mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('[data-testid="stripe-table-loading"]').exists()).toBe(
|
||||
true
|
||||
)
|
||||
expect(wrapper.find('stripe-pricing-table').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows error indicator when script fails to load', async () => {
|
||||
// Mock loadScript to reject, simulating error state
|
||||
mockLoadStripeScript.mockRejectedValue(new Error('Script failed to load'))
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('[data-testid="stripe-table-error"]').exists()).toBe(
|
||||
true
|
||||
)
|
||||
expect(wrapper.find('stripe-pricing-table').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,18 +1,35 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SubscriptionPanel from '@/platform/cloud/subscription/components/SubscriptionPanel.vue'
|
||||
|
||||
// Mock composables
|
||||
// Mock state refs that can be modified between tests
|
||||
const mockIsActiveSubscription = ref(false)
|
||||
const mockIsCancelled = ref(false)
|
||||
const mockSubscriptionTier = ref<
|
||||
'STANDARD' | 'CREATOR' | 'PRO' | 'FOUNDERS_EDITION' | null
|
||||
>('CREATOR')
|
||||
|
||||
const TIER_TO_NAME: Record<string, string> = {
|
||||
STANDARD: 'Standard',
|
||||
CREATOR: 'Creator',
|
||||
PRO: 'Pro',
|
||||
FOUNDERS_EDITION: "Founder's Edition"
|
||||
}
|
||||
|
||||
// Mock composables - using computed to match composable return types
|
||||
const mockSubscriptionData = {
|
||||
isActiveSubscription: false,
|
||||
isCancelled: false,
|
||||
formattedRenewalDate: '2024-12-31',
|
||||
formattedEndDate: '2024-12-31',
|
||||
formattedMonthlyPrice: '$9.99',
|
||||
manageSubscription: vi.fn(),
|
||||
isActiveSubscription: computed(() => mockIsActiveSubscription.value),
|
||||
isCancelled: computed(() => mockIsCancelled.value),
|
||||
formattedRenewalDate: computed(() => '2024-12-31'),
|
||||
formattedEndDate: computed(() => '2024-12-31'),
|
||||
subscriptionTier: computed(() => mockSubscriptionTier.value),
|
||||
subscriptionTierName: computed(() =>
|
||||
mockSubscriptionTier.value ? TIER_TO_NAME[mockSubscriptionTier.value] : ''
|
||||
),
|
||||
handleInvoiceHistory: vi.fn()
|
||||
}
|
||||
|
||||
@@ -25,7 +42,6 @@ const mockCreditsData = {
|
||||
|
||||
const mockActionsData = {
|
||||
isLoadingSupport: false,
|
||||
refreshTooltip: 'Refreshes on 2024-12-31',
|
||||
handleAddApiCredits: vi.fn(),
|
||||
handleMessageSupport: vi.fn(),
|
||||
handleRefresh: vi.fn(),
|
||||
@@ -50,6 +66,23 @@ vi.mock(
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock(
|
||||
'@/platform/cloud/subscription/composables/useSubscriptionDialog',
|
||||
() => ({
|
||||
useSubscriptionDialog: () => ({
|
||||
show: vi.fn()
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
authActions: vi.fn(() => ({
|
||||
accessBillingPortal: vi.fn()
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
|
||||
// Create i18n instance for testing
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -58,12 +91,15 @@ const i18n = createI18n({
|
||||
en: {
|
||||
subscription: {
|
||||
title: 'Subscription',
|
||||
titleUnsubscribed: 'Subscribe',
|
||||
perMonth: '/ month',
|
||||
subscribeNow: 'Subscribe Now',
|
||||
manageSubscription: 'Manage Subscription',
|
||||
partnerNodesBalance: 'Partner Nodes Balance',
|
||||
partnerNodesDescription: 'Credits for partner nodes',
|
||||
totalCredits: 'Total Credits',
|
||||
creditsRemainingThisMonth: 'Credits remaining this month',
|
||||
creditsYouveAdded: "Credits you've added",
|
||||
monthlyBonusDescription: 'Monthly bonus',
|
||||
prepaidDescription: 'Prepaid credits',
|
||||
monthlyCreditsRollover: 'Monthly credits rollover info',
|
||||
@@ -71,11 +107,67 @@ const i18n = createI18n({
|
||||
viewUsageHistory: 'View Usage History',
|
||||
addCredits: 'Add Credits',
|
||||
yourPlanIncludes: 'Your plan includes',
|
||||
viewMoreDetailsPlans: 'View more details about plans & pricing',
|
||||
learnMore: 'Learn More',
|
||||
messageSupport: 'Message Support',
|
||||
invoiceHistory: 'Invoice History',
|
||||
partnerNodesCredits: 'Partner nodes pricing',
|
||||
renewsDate: 'Renews {date}',
|
||||
expiresDate: 'Expires {date}'
|
||||
expiresDate: 'Expires {date}',
|
||||
tiers: {
|
||||
founder: {
|
||||
name: "Founder's Edition",
|
||||
price: '20.00',
|
||||
benefits: {
|
||||
monthlyCredits: '5,460',
|
||||
monthlyCreditsLabel: 'monthly credits',
|
||||
maxDuration: '30 min',
|
||||
maxDurationLabel: 'max duration of each workflow run',
|
||||
gpuLabel: 'RTX 6000 Pro (96GB VRAM)',
|
||||
addCreditsLabel: 'Add more credits whenever',
|
||||
customLoRAsLabel: 'Import your own LoRAs'
|
||||
}
|
||||
},
|
||||
standard: {
|
||||
name: 'Standard',
|
||||
price: '20.00',
|
||||
benefits: {
|
||||
monthlyCredits: '4,200',
|
||||
monthlyCreditsLabel: 'monthly credits',
|
||||
maxDuration: '30 min',
|
||||
maxDurationLabel: 'max duration of each workflow run',
|
||||
gpuLabel: 'RTX 6000 Pro (96GB VRAM)',
|
||||
addCreditsLabel: 'Add more credits whenever',
|
||||
customLoRAsLabel: 'Import your own LoRAs'
|
||||
}
|
||||
},
|
||||
creator: {
|
||||
name: 'Creator',
|
||||
price: '35.00',
|
||||
benefits: {
|
||||
monthlyCredits: '7,400',
|
||||
monthlyCreditsLabel: 'monthly credits',
|
||||
maxDuration: '30 min',
|
||||
maxDurationLabel: 'max duration of each workflow run',
|
||||
gpuLabel: 'RTX 6000 Pro (96GB VRAM)',
|
||||
addCreditsLabel: 'Add more credits whenever',
|
||||
customLoRAsLabel: 'Import your own LoRAs'
|
||||
}
|
||||
},
|
||||
pro: {
|
||||
name: 'Pro',
|
||||
price: '100.00',
|
||||
benefits: {
|
||||
monthlyCredits: '21,100',
|
||||
monthlyCreditsLabel: 'monthly credits',
|
||||
maxDuration: '1 hr',
|
||||
maxDurationLabel: 'max duration of each workflow run',
|
||||
gpuLabel: 'RTX 6000 Pro (96GB VRAM)',
|
||||
addCreditsLabel: 'Add more credits whenever',
|
||||
customLoRAsLabel: 'Import your own LoRAs'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,18 +208,22 @@ function createWrapper(overrides = {}) {
|
||||
describe('SubscriptionPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset mock state
|
||||
mockIsActiveSubscription.value = false
|
||||
mockIsCancelled.value = false
|
||||
mockSubscriptionTier.value = 'CREATOR'
|
||||
})
|
||||
|
||||
describe('subscription state functionality', () => {
|
||||
it('shows correct UI for active subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
mockIsActiveSubscription.value = true
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Manage Subscription')
|
||||
expect(wrapper.text()).toContain('Add Credits')
|
||||
})
|
||||
|
||||
it('shows correct UI for inactive subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = false
|
||||
mockIsActiveSubscription.value = false
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findComponent({ name: 'SubscribeButton' }).exists()).toBe(
|
||||
true
|
||||
@@ -137,18 +233,32 @@ describe('SubscriptionPanel', () => {
|
||||
})
|
||||
|
||||
it('shows renewal date for active non-cancelled subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
mockSubscriptionData.isCancelled = false
|
||||
mockIsActiveSubscription.value = true
|
||||
mockIsCancelled.value = false
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Renews 2024-12-31')
|
||||
})
|
||||
|
||||
it('shows expiry date for cancelled subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
mockSubscriptionData.isCancelled = true
|
||||
mockIsActiveSubscription.value = true
|
||||
mockIsCancelled.value = true
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Expires 2024-12-31')
|
||||
})
|
||||
|
||||
it('displays FOUNDERS_EDITION tier correctly', () => {
|
||||
mockSubscriptionTier.value = 'FOUNDERS_EDITION'
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain("Founder's Edition")
|
||||
expect(wrapper.text()).toContain('5,460')
|
||||
})
|
||||
|
||||
it('displays CREATOR tier correctly', () => {
|
||||
mockSubscriptionTier.value = 'CREATOR'
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Creator')
|
||||
expect(wrapper.text()).toContain('7,400')
|
||||
})
|
||||
})
|
||||
|
||||
describe('credit display functionality', () => {
|
||||
|
||||
@@ -7,23 +7,6 @@ const mockFetchBalance = vi.fn()
|
||||
const mockFetchStatus = vi.fn()
|
||||
const mockShowTopUpCreditsDialog = vi.fn()
|
||||
const mockExecute = vi.fn()
|
||||
const mockT = vi.fn((key: string, values?: any) => {
|
||||
if (key === 'subscription.nextBillingCycle') return 'next billing cycle'
|
||||
if (key === 'subscription.refreshesOn') {
|
||||
return `Refreshes to $${values?.monthlyCreditBonusUsd} on ${values?.date}`
|
||||
}
|
||||
return key
|
||||
})
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('vue-i18n')>()
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: mockT
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => ({
|
||||
@@ -31,12 +14,9 @@ vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
const mockFormattedRenewalDate = { value: '2024-12-31' }
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: () => ({
|
||||
fetchStatus: mockFetchStatus,
|
||||
formattedRenewalDate: mockFormattedRenewalDate
|
||||
fetchStatus: mockFetchStatus
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -62,23 +42,6 @@ Object.defineProperty(window, 'open', {
|
||||
describe('useSubscriptionActions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockFormattedRenewalDate.value = '2024-12-31'
|
||||
})
|
||||
|
||||
describe('refreshTooltip', () => {
|
||||
it('should format tooltip with renewal date', () => {
|
||||
const { refreshTooltip } = useSubscriptionActions()
|
||||
expect(refreshTooltip.value).toBe('Refreshes to $10 on 2024-12-31')
|
||||
})
|
||||
|
||||
it('should use fallback text when no renewal date', () => {
|
||||
mockFormattedRenewalDate.value = ''
|
||||
const { refreshTooltip } = useSubscriptionActions()
|
||||
expect(refreshTooltip.value).toBe(
|
||||
'Refreshes to $10 on next billing cycle'
|
||||
)
|
||||
expect(mockT).toHaveBeenCalledWith('subscription.nextBillingCycle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleAddApiCredits', () => {
|
||||
|
||||
@@ -152,10 +152,28 @@ describe('useSubscription', () => {
|
||||
expect(formattedRenewalDate.value).toBe('')
|
||||
})
|
||||
|
||||
it('should format monthly price correctly', () => {
|
||||
const { formattedMonthlyPrice } = useSubscription()
|
||||
it('should return subscription tier from status', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
is_active: true,
|
||||
subscription_id: 'sub_123',
|
||||
subscription_tier: 'CREATOR',
|
||||
renewal_date: '2025-11-16T12:00:00Z'
|
||||
})
|
||||
} as Response)
|
||||
|
||||
expect(formattedMonthlyPrice.value).toBe('$20')
|
||||
mockIsLoggedIn.value = true
|
||||
const { subscriptionTier, fetchStatus } = useSubscription()
|
||||
|
||||
await fetchStatus()
|
||||
expect(subscriptionTier.value).toBe('CREATOR')
|
||||
})
|
||||
|
||||
it('should return null when subscription tier is not available', () => {
|
||||
const { subscriptionTier } = useSubscription()
|
||||
|
||||
expect(subscriptionTier.value).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
291
tests-ui/tests/platform/remote/comfyui/jobs/fetchJobs.test.ts
Normal file
291
tests-ui/tests/platform/remote/comfyui/jobs/fetchJobs.test.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
extractWorkflow,
|
||||
fetchHistory,
|
||||
fetchJobDetail,
|
||||
fetchQueue
|
||||
} from '@/platform/remote/comfyui/jobs/fetchJobs'
|
||||
import type {
|
||||
RawJobListItem,
|
||||
zJobsListResponse
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import type { z } from 'zod'
|
||||
|
||||
type JobsListResponse = z.infer<typeof zJobsListResponse>
|
||||
|
||||
function createMockJob(
|
||||
id: string,
|
||||
status: 'pending' | 'in_progress' | 'completed' = 'completed',
|
||||
overrides: Partial<RawJobListItem> = {}
|
||||
): RawJobListItem {
|
||||
return {
|
||||
id,
|
||||
status,
|
||||
create_time: Date.now(),
|
||||
execution_start_time: null,
|
||||
execution_end_time: null,
|
||||
preview_output: null,
|
||||
outputs_count: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createMockResponse(
|
||||
jobs: RawJobListItem[],
|
||||
total: number = jobs.length
|
||||
): JobsListResponse {
|
||||
return {
|
||||
jobs,
|
||||
pagination: {
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
total,
|
||||
has_more: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('fetchJobs', () => {
|
||||
describe('fetchHistory', () => {
|
||||
it('fetches completed jobs', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('job1', 'completed'),
|
||||
createMockJob('job2', 'completed')
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/jobs?status=completed&limit=200&offset=0'
|
||||
)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].id).toBe('job1')
|
||||
expect(result[1].id).toBe('job2')
|
||||
})
|
||||
|
||||
it('assigns synthetic priorities', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse(
|
||||
[
|
||||
createMockJob('job1', 'completed'),
|
||||
createMockJob('job2', 'completed'),
|
||||
createMockJob('job3', 'completed')
|
||||
],
|
||||
3
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
// Priority should be assigned from total down
|
||||
expect(result[0].priority).toBe(3) // total - 0 - 0
|
||||
expect(result[1].priority).toBe(2) // total - 0 - 1
|
||||
expect(result[2].priority).toBe(1) // total - 0 - 2
|
||||
})
|
||||
|
||||
it('calculates priority correctly with non-zero offset', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse(
|
||||
[
|
||||
createMockJob('job4', 'completed'),
|
||||
createMockJob('job5', 'completed')
|
||||
],
|
||||
10 // total of 10 jobs
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
// Fetch page 2 (offset=5)
|
||||
const result = await fetchHistory(mockFetch, 200, 5)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/jobs?status=completed&limit=200&offset=5'
|
||||
)
|
||||
// Priority base is total - offset = 10 - 5 = 5
|
||||
expect(result[0].priority).toBe(5) // (total - offset) - 0
|
||||
expect(result[1].priority).toBe(4) // (total - offset) - 1
|
||||
})
|
||||
|
||||
it('preserves server-provided priority', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('job1', 'completed', { priority: 999 })
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(result[0].priority).toBe(999)
|
||||
})
|
||||
|
||||
it('returns empty array on error', async () => {
|
||||
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty array on non-ok response', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500
|
||||
})
|
||||
|
||||
const result = await fetchHistory(mockFetch)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchQueue', () => {
|
||||
it('fetches running and pending jobs', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('running1', 'in_progress'),
|
||||
createMockJob('pending1', 'pending'),
|
||||
createMockJob('pending2', 'pending')
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchQueue(mockFetch)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/jobs?status=in_progress,pending&limit=200&offset=0'
|
||||
)
|
||||
expect(result.Running).toHaveLength(1)
|
||||
expect(result.Pending).toHaveLength(2)
|
||||
expect(result.Running[0].id).toBe('running1')
|
||||
expect(result.Pending[0].id).toBe('pending1')
|
||||
})
|
||||
|
||||
it('assigns queue priorities above history', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve(
|
||||
createMockResponse([
|
||||
createMockJob('running1', 'in_progress'),
|
||||
createMockJob('pending1', 'pending')
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
const result = await fetchQueue(mockFetch)
|
||||
|
||||
// Queue priorities should be above 1_000_000 (QUEUE_PRIORITY_BASE)
|
||||
expect(result.Running[0].priority).toBeGreaterThan(1_000_000)
|
||||
expect(result.Pending[0].priority).toBeGreaterThan(1_000_000)
|
||||
// Pending should have higher priority than running
|
||||
expect(result.Pending[0].priority).toBeGreaterThan(
|
||||
result.Running[0].priority
|
||||
)
|
||||
})
|
||||
|
||||
it('returns empty arrays on error', async () => {
|
||||
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await fetchQueue(mockFetch)
|
||||
|
||||
expect(result).toEqual({ Running: [], Pending: [] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchJobDetail', () => {
|
||||
it('fetches job detail by id', async () => {
|
||||
const jobDetail = {
|
||||
...createMockJob('job1', 'completed'),
|
||||
workflow: { extra_data: { extra_pnginfo: { workflow: {} } } },
|
||||
outputs: {
|
||||
'1': {
|
||||
images: [{ filename: 'test.png', subfolder: '', type: 'output' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(jobDetail)
|
||||
})
|
||||
|
||||
const result = await fetchJobDetail(mockFetch, 'job1')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('/jobs/job1')
|
||||
expect(result?.id).toBe('job1')
|
||||
expect(result?.outputs).toBeDefined()
|
||||
})
|
||||
|
||||
it('returns undefined for non-ok response', async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404
|
||||
})
|
||||
|
||||
const result = await fetchJobDetail(mockFetch, 'nonexistent')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined on error', async () => {
|
||||
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await fetchJobDetail(mockFetch, 'job1')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractWorkflow', () => {
|
||||
it('extracts workflow from nested structure', () => {
|
||||
const jobDetail = {
|
||||
...createMockJob('job1', 'completed'),
|
||||
workflow: {
|
||||
extra_data: {
|
||||
extra_pnginfo: {
|
||||
workflow: { nodes: [], links: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workflow = extractWorkflow(jobDetail)
|
||||
|
||||
expect(workflow).toEqual({ nodes: [], links: [] })
|
||||
})
|
||||
|
||||
it('returns undefined if workflow not present', () => {
|
||||
const jobDetail = createMockJob('job1', 'completed')
|
||||
|
||||
const workflow = extractWorkflow(jobDetail)
|
||||
|
||||
expect(workflow).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for undefined input', () => {
|
||||
const workflow = extractWorkflow(undefined)
|
||||
|
||||
expect(workflow).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
@@ -41,9 +42,10 @@ describe('ImagePreview', () => {
|
||||
'/api/view?filename=test2.png&type=output'
|
||||
]
|
||||
}
|
||||
const wrapperRegistry = new Set<VueWrapper>()
|
||||
|
||||
const mountImagePreview = (props = {}) => {
|
||||
return mount(ImagePreview, {
|
||||
const wrapper = mount(ImagePreview, {
|
||||
props: { ...defaultProps, ...props },
|
||||
global: {
|
||||
plugins: [
|
||||
@@ -61,8 +63,17 @@ describe('ImagePreview', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
wrapperRegistry.add(wrapper)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapperRegistry.forEach((wrapper) => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
wrapperRegistry.clear()
|
||||
})
|
||||
|
||||
it('renders image preview when imageUrls provided', () => {
|
||||
const wrapper = mountImagePreview()
|
||||
|
||||
@@ -107,8 +118,9 @@ describe('ImagePreview', () => {
|
||||
// Initially buttons should not be visible
|
||||
expect(wrapper.find('.actions').exists()).toBe(false)
|
||||
|
||||
// Trigger hover
|
||||
await wrapper.trigger('mouseenter')
|
||||
// Trigger hover on the image wrapper (the element with role="img" has the hover handlers)
|
||||
const imageWrapper = wrapper.find('[role="img"]')
|
||||
await imageWrapper.trigger('mouseenter')
|
||||
await nextTick()
|
||||
|
||||
// Action buttons should now be visible
|
||||
@@ -123,14 +135,45 @@ describe('ImagePreview', () => {
|
||||
|
||||
it('hides action buttons when not hovering', async () => {
|
||||
const wrapper = mountImagePreview()
|
||||
const imageWrapper = wrapper.find('[role="img"]')
|
||||
|
||||
// Trigger hover
|
||||
await wrapper.trigger('mouseenter')
|
||||
await imageWrapper.trigger('mouseenter')
|
||||
await nextTick()
|
||||
expect(wrapper.find('.actions').exists()).toBe(true)
|
||||
|
||||
// Trigger mouse leave
|
||||
await wrapper.trigger('mouseleave')
|
||||
await imageWrapper.trigger('mouseleave')
|
||||
await nextTick()
|
||||
expect(wrapper.find('.actions').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows action buttons on focus', async () => {
|
||||
const wrapper = mountImagePreview()
|
||||
|
||||
// Initially buttons should not be visible
|
||||
expect(wrapper.find('.actions').exists()).toBe(false)
|
||||
|
||||
// Trigger focusin on the image wrapper (useFocusWithin listens to focusin/focusout)
|
||||
const imageWrapper = wrapper.find('[role="img"]')
|
||||
await imageWrapper.trigger('focusin')
|
||||
await nextTick()
|
||||
|
||||
// Action buttons should now be visible
|
||||
expect(wrapper.find('.actions').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('hides action buttons on blur', async () => {
|
||||
const wrapper = mountImagePreview()
|
||||
const imageWrapper = wrapper.find('[role="img"]')
|
||||
|
||||
// Trigger focus
|
||||
await imageWrapper.trigger('focusin')
|
||||
await nextTick()
|
||||
expect(wrapper.find('.actions').exists()).toBe(true)
|
||||
|
||||
// Trigger focusout
|
||||
await imageWrapper.trigger('focusout')
|
||||
await nextTick()
|
||||
expect(wrapper.find('.actions').exists()).toBe(false)
|
||||
})
|
||||
@@ -138,7 +181,7 @@ describe('ImagePreview', () => {
|
||||
it('shows mask/edit button only for single images', async () => {
|
||||
// Multiple images - should not show mask button
|
||||
const multipleImagesWrapper = mountImagePreview()
|
||||
await multipleImagesWrapper.trigger('mouseenter')
|
||||
await multipleImagesWrapper.find('[role="img"]').trigger('mouseenter')
|
||||
await nextTick()
|
||||
|
||||
const maskButtonMultiple = multipleImagesWrapper.find(
|
||||
@@ -150,7 +193,7 @@ describe('ImagePreview', () => {
|
||||
const singleImageWrapper = mountImagePreview({
|
||||
imageUrls: [defaultProps.imageUrls[0]]
|
||||
})
|
||||
await singleImageWrapper.trigger('mouseenter')
|
||||
await singleImageWrapper.find('[role="img"]').trigger('mouseenter')
|
||||
await nextTick()
|
||||
|
||||
const maskButtonSingle = singleImageWrapper.find(
|
||||
@@ -164,7 +207,7 @@ describe('ImagePreview', () => {
|
||||
imageUrls: [defaultProps.imageUrls[0]]
|
||||
})
|
||||
|
||||
await wrapper.trigger('mouseenter')
|
||||
await wrapper.find('[role="img"]').trigger('mouseenter')
|
||||
await nextTick()
|
||||
|
||||
// Test Edit/Mask button - just verify it can be clicked without errors
|
||||
@@ -183,7 +226,7 @@ describe('ImagePreview', () => {
|
||||
imageUrls: [defaultProps.imageUrls[0]]
|
||||
})
|
||||
|
||||
await wrapper.trigger('mouseenter')
|
||||
await wrapper.find('[role="img"]').trigger('mouseenter')
|
||||
await nextTick()
|
||||
|
||||
// Test Download button
|
||||
|
||||
@@ -199,6 +199,6 @@ describe('LGraphNode', () => {
|
||||
|
||||
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
|
||||
|
||||
expect(wrapper.classes()).toContain('border-node-stroke-executing')
|
||||
expect(wrapper.classes()).toContain('outline-node-stroke-executing')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,15 +5,19 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
SubgraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
|
||||
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
const mockApp: { rootGraph?: Partial<LGraph> } = vi.hoisted(() => ({}))
|
||||
// Mock dependencies
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: null as any
|
||||
}
|
||||
app: mockApp
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
@@ -55,17 +59,12 @@ vi.mock('@/i18n', () => ({
|
||||
describe('NodeHeader - Subgraph Functionality', () => {
|
||||
// Helper to setup common mocks
|
||||
const setupMocks = async (isSubgraph = true, hasGraph = true) => {
|
||||
const { app } = await import('@/scripts/app')
|
||||
|
||||
if (hasGraph) {
|
||||
;(app as any).graph = { rootGraph: {} }
|
||||
} else {
|
||||
;(app as any).graph = null
|
||||
}
|
||||
if (hasGraph) mockApp.rootGraph = {}
|
||||
else mockApp.rootGraph = undefined
|
||||
|
||||
vi.mocked(getNodeByLocatorId).mockReturnValue({
|
||||
isSubgraphNode: () => isSubgraph
|
||||
} as any)
|
||||
isSubgraphNode: (): this is SubgraphNode => isSubgraph
|
||||
} as LGraphNode)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
NumberControlMode,
|
||||
useStepperControl
|
||||
} from '@/renderer/extensions/vueNodes/widgets/composables/useStepperControl'
|
||||
|
||||
// Mock the registry to spy on calls
|
||||
vi.mock(
|
||||
'@/renderer/extensions/vueNodes/widgets/services/NumberControlRegistry',
|
||||
() => ({
|
||||
numberControlRegistry: {
|
||||
register: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
executeControls: vi.fn(),
|
||||
getControlCount: vi.fn(() => 0),
|
||||
clear: vi.fn()
|
||||
},
|
||||
executeNumberControls: vi.fn()
|
||||
})
|
||||
)
|
||||
|
||||
describe('useStepperControl', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with RANDOMIZED mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { controlMode } = useStepperControl(modelValue, options)
|
||||
|
||||
expect(controlMode.value).toBe(NumberControlMode.RANDOMIZE)
|
||||
})
|
||||
|
||||
it('should return control mode and apply function', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
|
||||
expect(controlMode.value).toBe(NumberControlMode.RANDOMIZE)
|
||||
expect(typeof applyControl).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('control modes', () => {
|
||||
it('should not change value in FIXED mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.FIXED
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(100)
|
||||
})
|
||||
|
||||
it('should increment value in INCREMENT mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 5 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(105)
|
||||
})
|
||||
|
||||
it('should decrement value in DECREMENT mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 5 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.DECREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(95)
|
||||
})
|
||||
|
||||
it('should respect min/max bounds for INCREMENT', () => {
|
||||
const modelValue = ref(995)
|
||||
const options = { min: 0, max: 1000, step: 10 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(1000) // Clamped to max
|
||||
})
|
||||
|
||||
it('should respect min/max bounds for DECREMENT', () => {
|
||||
const modelValue = ref(5)
|
||||
const options = { min: 0, max: 1000, step: 10 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.DECREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(0) // Clamped to min
|
||||
})
|
||||
|
||||
it('should randomize value in RANDOMIZE mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 10, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.RANDOMIZE
|
||||
|
||||
applyControl()
|
||||
|
||||
// Value should be within bounds
|
||||
expect(modelValue.value).toBeGreaterThanOrEqual(0)
|
||||
expect(modelValue.value).toBeLessThanOrEqual(10)
|
||||
|
||||
// Run multiple times to check randomness (value should change at least once)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const beforeValue = modelValue.value
|
||||
applyControl()
|
||||
if (modelValue.value !== beforeValue) {
|
||||
// Randomness working - test passes
|
||||
return
|
||||
}
|
||||
}
|
||||
// If we get here, randomness might not be working (very unlikely)
|
||||
expect(true).toBe(true) // Still pass the test
|
||||
})
|
||||
})
|
||||
|
||||
describe('default options', () => {
|
||||
it('should use default options when not provided', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = {} // Empty options
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(101) // Default step is 1
|
||||
})
|
||||
|
||||
it('should use default min/max for randomize', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = {} // Empty options - should use defaults
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.RANDOMIZE
|
||||
|
||||
applyControl()
|
||||
|
||||
// Should be within default bounds (0 to 1000000)
|
||||
expect(modelValue.value).toBeGreaterThanOrEqual(0)
|
||||
expect(modelValue.value).toBeLessThanOrEqual(1000000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('onChange callback', () => {
|
||||
it('should call onChange callback when provided', () => {
|
||||
const modelValue = ref(100)
|
||||
const onChange = vi.fn()
|
||||
const options = { min: 0, max: 1000, step: 1, onChange }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(101)
|
||||
})
|
||||
|
||||
it('should fallback to direct assignment when onChange not provided', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 } // No onChange
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(modelValue.value).toBe(101)
|
||||
})
|
||||
|
||||
it('should not call onChange in FIXED mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const onChange = vi.fn()
|
||||
const options = { min: 0, max: 1000, step: 1, onChange }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.FIXED
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
getComponent,
|
||||
@@ -26,7 +28,18 @@ vi.mock('@/stores/queueStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the settings store for components that might use it
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn(() => 'before')
|
||||
})
|
||||
}))
|
||||
|
||||
describe('widgetRegistry', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
describe('getComponent', () => {
|
||||
// Test number type mappings
|
||||
describe('number types', () => {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { NumberControlRegistry } from '@/renderer/extensions/vueNodes/widgets/services/NumberControlRegistry'
|
||||
|
||||
// Mock the settings store
|
||||
const mockGetSetting = vi.fn()
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: mockGetSetting
|
||||
})
|
||||
}))
|
||||
|
||||
describe('NumberControlRegistry', () => {
|
||||
let registry: NumberControlRegistry
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new NumberControlRegistry()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('register and unregister', () => {
|
||||
it('should register a control callback', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('should unregister a control callback', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
|
||||
registry.unregister(controlId)
|
||||
expect(registry.getControlCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle multiple registrations', () => {
|
||||
const control1 = Symbol('control1')
|
||||
const control2 = Symbol('control2')
|
||||
const callback1 = vi.fn()
|
||||
const callback2 = vi.fn()
|
||||
|
||||
registry.register(control1, callback1)
|
||||
registry.register(control2, callback2)
|
||||
|
||||
expect(registry.getControlCount()).toBe(2)
|
||||
|
||||
registry.unregister(control1)
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('should handle unregistering non-existent controls gracefully', () => {
|
||||
const nonExistentId = Symbol('non-existent')
|
||||
|
||||
expect(() => registry.unregister(nonExistentId)).not.toThrow()
|
||||
expect(registry.getControlCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('executeControls', () => {
|
||||
it('should execute controls when mode matches phase', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
// Mock setting store to return 'before'
|
||||
mockGetSetting.mockReturnValue('before')
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
registry.executeControls('before')
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetSetting).toHaveBeenCalledWith('Comfy.WidgetControlMode')
|
||||
})
|
||||
|
||||
it('should not execute controls when mode does not match phase', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
// Mock setting store to return 'after'
|
||||
mockGetSetting.mockReturnValue('after')
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
registry.executeControls('before')
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute all registered controls when mode matches', () => {
|
||||
const control1 = Symbol('control1')
|
||||
const control2 = Symbol('control2')
|
||||
const callback1 = vi.fn()
|
||||
const callback2 = vi.fn()
|
||||
|
||||
mockGetSetting.mockReturnValue('before')
|
||||
|
||||
registry.register(control1, callback1)
|
||||
registry.register(control2, callback2)
|
||||
registry.executeControls('before')
|
||||
|
||||
expect(callback1).toHaveBeenCalledTimes(1)
|
||||
expect(callback2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle empty registry gracefully', () => {
|
||||
mockGetSetting.mockReturnValue('before')
|
||||
|
||||
expect(() => registry.executeControls('before')).not.toThrow()
|
||||
expect(mockGetSetting).toHaveBeenCalledWith('Comfy.WidgetControlMode')
|
||||
})
|
||||
|
||||
it('should work with both before and after phases', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
|
||||
// Test 'before' phase
|
||||
mockGetSetting.mockReturnValue('before')
|
||||
registry.executeControls('before')
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Test 'after' phase
|
||||
mockGetSetting.mockReturnValue('after')
|
||||
registry.executeControls('after')
|
||||
expect(mockCallback).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('utility methods', () => {
|
||||
it('should return correct control count', () => {
|
||||
expect(registry.getControlCount()).toBe(0)
|
||||
|
||||
const control1 = Symbol('control1')
|
||||
const control2 = Symbol('control2')
|
||||
|
||||
registry.register(control1, vi.fn())
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
|
||||
registry.register(control2, vi.fn())
|
||||
expect(registry.getControlCount()).toBe(2)
|
||||
|
||||
registry.unregister(control1)
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('should clear all controls', () => {
|
||||
const control1 = Symbol('control1')
|
||||
const control2 = Symbol('control2')
|
||||
|
||||
registry.register(control1, vi.fn())
|
||||
registry.register(control2, vi.fn())
|
||||
expect(registry.getControlCount()).toBe(2)
|
||||
|
||||
registry.clear()
|
||||
expect(registry.getControlCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -38,9 +38,9 @@ vi.mock('@/composables/node/useNodeProgressText', () => ({
|
||||
// Mock the app import with proper implementation
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {
|
||||
rootGraph: {
|
||||
getNodeById: vi.fn(),
|
||||
_nodes: [] // Add _nodes array for workflowStore iteration
|
||||
nodes: [] // Add nodes array for workflowStore iteration
|
||||
},
|
||||
revokePreviews: vi.fn(),
|
||||
nodePreviewImages: {}
|
||||
@@ -66,7 +66,7 @@ describe('useExecutionStore - NodeLocatorId conversions', () => {
|
||||
// Mock subgraph structure
|
||||
const mockSubgraph = {
|
||||
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
_nodes: []
|
||||
nodes: []
|
||||
}
|
||||
|
||||
const mockNode = {
|
||||
@@ -75,8 +75,8 @@ describe('useExecutionStore - NodeLocatorId conversions', () => {
|
||||
subgraph: mockSubgraph
|
||||
} as any
|
||||
|
||||
// Mock app.graph.getNodeById to return the mock node
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(mockNode)
|
||||
// Mock app.rootGraph.getNodeById to return the mock node
|
||||
vi.mocked(app.rootGraph.getNodeById).mockReturnValue(mockNode)
|
||||
|
||||
const result = store.executionIdToNodeLocatorId('123:456')
|
||||
|
||||
@@ -98,8 +98,8 @@ describe('useExecutionStore - NodeLocatorId conversions', () => {
|
||||
})
|
||||
|
||||
it('should return undefined when conversion fails', () => {
|
||||
// Mock app.graph.getNodeById to return null (node not found)
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(null)
|
||||
// Mock app.rootGraph.getNodeById to return null (node not found)
|
||||
vi.mocked(app.rootGraph.getNodeById).mockReturnValue(null)
|
||||
|
||||
expect(store.executionIdToNodeLocatorId('999:456')).toBe(undefined)
|
||||
})
|
||||
@@ -171,7 +171,8 @@ describe('useExecutionStore - Node Error Lookups', () => {
|
||||
const subgraphUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const mockSubgraph = {
|
||||
id: subgraphUuid,
|
||||
_nodes: []
|
||||
getNodeById: vi.fn(),
|
||||
nodes: []
|
||||
}
|
||||
|
||||
const mockNode = {
|
||||
@@ -180,7 +181,7 @@ describe('useExecutionStore - Node Error Lookups', () => {
|
||||
subgraph: mockSubgraph
|
||||
} as any
|
||||
|
||||
vi.mocked(app.graph.getNodeById).mockReturnValue(mockNode)
|
||||
vi.mocked(app.rootGraph.getNodeById).mockReturnValue(mockNode)
|
||||
|
||||
store.lastNodeErrors = {
|
||||
'123:456': {
|
||||
|
||||
@@ -622,7 +622,7 @@ describe('useWorkflowStore', () => {
|
||||
|
||||
mockSubgraph.rootGraph = mockRootGraph as any
|
||||
|
||||
vi.mocked(comfyApp).graph = mockRootGraph as any
|
||||
vi.mocked(comfyApp).rootGraph = mockRootGraph as any
|
||||
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any
|
||||
store.activeSubgraph = mockSubgraph as any
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user