mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-08 15:29:52 +00:00
Compare commits
10 Commits
test/cover
...
test/cov-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd17fcf8c6 | ||
|
|
cfc65bda75 | ||
|
|
f7f237658c | ||
|
|
f88492387b | ||
|
|
c09f1d7d15 | ||
|
|
4a40c050d9 | ||
|
|
35492bc530 | ||
|
|
c30177a749 | ||
|
|
cb443d455e | ||
|
|
a378ebb5af |
@@ -1,43 +1,600 @@
|
|||||||
import { createTestingPinia } from '@pinia/testing'
|
|
||||||
import { setActivePinia } from 'pinia'
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
vi.mock('@/scripts/app', () => ({
|
import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph'
|
||||||
app: { canvas: undefined },
|
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
ComfyApp: class {}
|
import type { ContextMenuDivElement } from '@/lib/litegraph/src/interfaces'
|
||||||
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
|
|
||||||
|
import { getExtraOptionsForWidget } from '@/services/litegraphService'
|
||||||
|
|
||||||
|
async function invokeMenuCallback(option: IContextMenuValue): Promise<void> {
|
||||||
|
// Production callbacks under test do not reference `this`; ContextMenuDivElement
|
||||||
|
// is a DOM element decorated with extra fields, not realistic to construct in tests.
|
||||||
|
await option.callback?.call({} as ContextMenuDivElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockPrompt = vi.fn()
|
||||||
|
const mockCanvas = vi.hoisted(() => ({
|
||||||
|
setDirty: vi.fn(),
|
||||||
|
graph_mouse: [100, 200],
|
||||||
|
ds: {
|
||||||
|
scale: 1,
|
||||||
|
offset: [0, 0] as [number, number],
|
||||||
|
visible_area: [0, 0, 800, 600] as
|
||||||
|
| [number, number, number, number]
|
||||||
|
| undefined,
|
||||||
|
fitToBounds: vi.fn()
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
nodes: [] as unknown[],
|
||||||
|
getNodeById: vi.fn(),
|
||||||
|
add: vi.fn(),
|
||||||
|
setDirtyCanvas: vi.fn(),
|
||||||
|
isRootGraph: true
|
||||||
|
},
|
||||||
|
animateToBounds: vi.fn(),
|
||||||
|
_deserializeItems: vi.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import { app } from '@/scripts/app'
|
const mockApp = vi.hoisted(() => ({
|
||||||
import { useLitegraphService } from '@/services/litegraphService'
|
canvas: undefined as unknown,
|
||||||
|
graph: undefined as unknown,
|
||||||
|
dragOverNode: null,
|
||||||
|
lastExecutionError: null,
|
||||||
|
rootGraph: {}
|
||||||
|
}))
|
||||||
|
|
||||||
describe('useLitegraphService().getCanvasCenter', () => {
|
const mockFavoritedWidgetsStore = vi.hoisted(() => ({
|
||||||
|
isFavorited: vi.fn().mockReturnValue(false),
|
||||||
|
toggleFavorite: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/workspace/favoritedWidgetsStore', () => ({
|
||||||
|
useFavoritedWidgetsStore: () => mockFavoritedWidgetsStore
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/services/dialogService', () => ({
|
||||||
|
useDialogService: () => ({
|
||||||
|
prompt: mockPrompt
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||||
|
useCanvasStore: () => ({
|
||||||
|
canvas: mockCanvas,
|
||||||
|
getCanvas: () => mockCanvas
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/core/graph/subgraph/promotionUtils', () => ({
|
||||||
|
addWidgetPromotionOptions: vi.fn(),
|
||||||
|
isPreviewPseudoWidget: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/i18n', () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
st: (_key: string, fallback: string) => fallback
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/formatUtil', () => ({
|
||||||
|
normalizeI18nKey: (key: string) => key
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/scripts/app', () => ({
|
||||||
|
app: mockApp,
|
||||||
|
ComfyApp: {
|
||||||
|
clipspace: null,
|
||||||
|
clipspace_return_node: null,
|
||||||
|
copyToClipspace: vi.fn(),
|
||||||
|
pasteFromClipspace: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||||
|
useToastStore: () => ({ addAlert: vi.fn() })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/widgetStore', () => ({
|
||||||
|
useWidgetStore: () => ({ widgets: new Map() })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/executionStore', () => ({
|
||||||
|
useExecutionStore: () => ({
|
||||||
|
nodeLocationProgressStates: {}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||||
|
useWorkflowStore: () => ({
|
||||||
|
activeSubgraph: null,
|
||||||
|
nodeIdToNodeLocatorId: (id: string) => id
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/platform/settings/settingStore', () => ({
|
||||||
|
useSettingStore: () => ({
|
||||||
|
get: vi.fn().mockReturnValue(false)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/canvas/useSelectedLiteGraphItems', () => ({
|
||||||
|
useSelectedLiteGraphItems: () => ({
|
||||||
|
toggleSelectedNodesMode: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/services/extensionService', () => ({
|
||||||
|
useExtensionService: () => ({
|
||||||
|
invokeExtensionsAsync: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/subgraphStore', () => ({
|
||||||
|
useSubgraphStore: () => ({
|
||||||
|
typePrefix: 'Subgraph::',
|
||||||
|
getBlueprint: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockNodeOutputStore = vi.hoisted(() => ({
|
||||||
|
getNodeOutputs: vi.fn(),
|
||||||
|
getNodePreviews: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/nodeOutputStore', () => ({
|
||||||
|
useNodeOutputStore: () => mockNodeOutputStore
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/node/useNodeAnimatedImage', () => ({
|
||||||
|
useNodeAnimatedImage: () => ({
|
||||||
|
showAnimatedPreview: vi.fn(),
|
||||||
|
removeAnimatedPreview: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/node/useNodeCanvasImagePreview', () => ({
|
||||||
|
useNodeCanvasImagePreview: () => ({
|
||||||
|
showCanvasImagePreview: vi.fn(),
|
||||||
|
removeCanvasImagePreview: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/node/useNodeImage', () => ({
|
||||||
|
useNodeImage: () => ({ showPreview: vi.fn() }),
|
||||||
|
useNodeVideo: () => ({ showPreview: vi.fn() })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/graph/useSubgraphOperations', () => ({
|
||||||
|
useSubgraphOperations: () => ({ unpackSubgraph: vi.fn() })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/maskeditor/useMaskEditor', () => ({
|
||||||
|
useMaskEditor: () => ({ openMaskEditor: vi.fn() })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/domWidgetStore', () => ({
|
||||||
|
useDomWidgetStore: () => ({
|
||||||
|
widgetStates: new Map(),
|
||||||
|
registerWidget: vi.fn(),
|
||||||
|
unregisterWidget: vi.fn()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/promotionStore', () => ({
|
||||||
|
usePromotionStore: () => ({
|
||||||
|
getPromotionsRef: vi.fn().mockReturnValue([])
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/services/subgraphPseudoWidgetCache', () => ({
|
||||||
|
resolveSubgraphPseudoWidgetCache: vi.fn().mockReturnValue({
|
||||||
|
cache: { promotions: [], entries: [], nodes: [] },
|
||||||
|
nodes: []
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/workspace/rightSidePanelStore', () => ({
|
||||||
|
useRightSidePanelStore: () => ({ openPanel: vi.fn() })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/base/common/downloadUtil', () => ({
|
||||||
|
downloadFile: vi.fn(),
|
||||||
|
openFileInNewTab: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/scripts/domWidget', () => ({
|
||||||
|
isComponentWidget: vi.fn().mockReturnValue(false),
|
||||||
|
isDOMWidget: vi.fn().mockReturnValue(false)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockCreateBounds = vi.hoisted(() => vi.fn())
|
||||||
|
vi.mock('@/lib/litegraph/src/litegraph', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal()
|
||||||
|
return {
|
||||||
|
...(actual as object),
|
||||||
|
createBounds: mockCreateBounds
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/scripts/ui', () => ({
|
||||||
|
$el: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/litegraphUtil', () => ({
|
||||||
|
isAnimatedOutput: vi.fn().mockReturnValue(false),
|
||||||
|
isImageNode: vi.fn().mockReturnValue(false),
|
||||||
|
isVideoNode: vi.fn().mockReturnValue(false),
|
||||||
|
isVideoOutput: vi.fn().mockReturnValue(false),
|
||||||
|
migrateWidgetsValues: vi.fn().mockReturnValue([])
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/core/graph/widgets/dynamicWidgets', () => ({
|
||||||
|
applyDynamicInputs: vi.fn().mockReturnValue(false)
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/schemas/nodeDef/migration', () => ({
|
||||||
|
transformInputSpecV2ToV1: vi.fn().mockReturnValue([])
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/workbench/utils/nodeDefOrderingUtil', () => ({
|
||||||
|
getOrderedInputSpecs: vi.fn().mockReturnValue([])
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/nodeDefStore', () => ({
|
||||||
|
ComfyNodeDefImpl: vi.fn().mockImplementation((def: unknown) => def)
|
||||||
|
}))
|
||||||
|
|
||||||
|
function createMockNode(overrides: Record<string, unknown> = {}): LGraphNode {
|
||||||
|
const node = new LGraphNode('TestNode')
|
||||||
|
Object.assign(node, {
|
||||||
|
id: 1,
|
||||||
|
inputs: [],
|
||||||
|
graph: null,
|
||||||
|
getWidgetOnPos: vi.fn()
|
||||||
|
})
|
||||||
|
Object.assign(node, overrides)
|
||||||
|
// Set static nodeData for tests that check constructor.nodeData
|
||||||
|
;(node.constructor as { nodeData?: { name: string } }).nodeData = {
|
||||||
|
name: 'TestNode'
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockWidget(
|
||||||
|
overrides: Record<string, unknown> = {}
|
||||||
|
): IBaseWidget {
|
||||||
|
return {
|
||||||
|
name: 'test_widget',
|
||||||
|
label: undefined,
|
||||||
|
value: 42,
|
||||||
|
callback: vi.fn(),
|
||||||
|
options: {},
|
||||||
|
...overrides
|
||||||
|
} as unknown as IBaseWidget
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('litegraphService', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
vi.clearAllMocks()
|
||||||
|
mockFavoritedWidgetsStore.isFavorited.mockReturnValue(false)
|
||||||
|
mockPrompt.mockReset()
|
||||||
|
mockCreateBounds.mockReset()
|
||||||
|
mockCanvas.graph.getNodeById.mockReset()
|
||||||
|
mockCanvas.ds.scale = 1
|
||||||
|
mockCanvas.ds.offset = [0, 0]
|
||||||
|
mockCanvas.ds.visible_area = [0, 0, 800, 600]
|
||||||
|
mockCanvas.graph.nodes = []
|
||||||
|
mockApp.canvas = mockCanvas
|
||||||
|
mockApp.graph = mockCanvas.graph
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns origin when canvas is not yet initialised', () => {
|
describe('getExtraOptionsForWidget', () => {
|
||||||
Reflect.set(app, 'canvas', undefined)
|
it('adds favorite option when widget is not favorited', () => {
|
||||||
|
const node = createMockNode()
|
||||||
|
const widget = createMockWidget()
|
||||||
|
mockFavoritedWidgetsStore.isFavorited.mockReturnValue(false)
|
||||||
|
|
||||||
const center = useLitegraphService().getCanvasCenter()
|
const options = getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
expect(center).toEqual([0, 0])
|
expect(options).toHaveLength(1)
|
||||||
})
|
expect(options[0].content).toContain('contextMenu.FavoriteWidget')
|
||||||
|
expect(options[0].content).toContain('test_widget')
|
||||||
it('returns origin when canvas exists but ds.visible_area is missing', () => {
|
|
||||||
Reflect.set(app, 'canvas', { ds: {} })
|
|
||||||
|
|
||||||
const center = useLitegraphService().getCanvasCenter()
|
|
||||||
|
|
||||||
expect(center).toEqual([0, 0])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns the visible-area centre once the canvas is ready', () => {
|
|
||||||
Reflect.set(app, 'canvas', {
|
|
||||||
ds: { visible_area: [10, 20, 200, 100] }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const center = useLitegraphService().getCanvasCenter()
|
it('adds unfavorite option when widget is already favorited', () => {
|
||||||
|
const node = createMockNode()
|
||||||
|
const widget = createMockWidget()
|
||||||
|
mockFavoritedWidgetsStore.isFavorited.mockReturnValue(true)
|
||||||
|
|
||||||
expect(center).toEqual([110, 70])
|
const options = getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
expect(options[0].content).toContain('contextMenu.UnfavoriteWidget')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses widget label when available', () => {
|
||||||
|
const node = createMockNode()
|
||||||
|
const widget = createMockWidget({ label: 'My Label' })
|
||||||
|
mockFavoritedWidgetsStore.isFavorited.mockReturnValue(false)
|
||||||
|
|
||||||
|
const options = getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
expect(options[0].content).toContain('My Label')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls toggleFavorite when favorite option callback is invoked', () => {
|
||||||
|
const node = createMockNode()
|
||||||
|
const widget = createMockWidget()
|
||||||
|
|
||||||
|
const options = getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
void invokeMenuCallback(options[0])
|
||||||
|
expect(mockFavoritedWidgetsStore.toggleFavorite).toHaveBeenCalledWith(
|
||||||
|
node,
|
||||||
|
'test_widget'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds rename option when input matches widget', () => {
|
||||||
|
const widget = createMockWidget({ name: 'seed' })
|
||||||
|
const node = createMockNode({
|
||||||
|
inputs: [{ widget: { name: 'seed' } }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
// rename is unshifted first, then favorite is unshifted (ends up first)
|
||||||
|
expect(options).toHaveLength(2)
|
||||||
|
const renameOption = options.find((o: IContextMenuValue) =>
|
||||||
|
o.content?.includes('contextMenu.RenameWidget')
|
||||||
|
)
|
||||||
|
expect(renameOption).toBeDefined()
|
||||||
|
expect(renameOption!.content).toContain('seed')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rename callback updates widget and input labels', async () => {
|
||||||
|
const widget = createMockWidget({ name: 'seed' })
|
||||||
|
const input = { widget: { name: 'seed' }, label: undefined as unknown }
|
||||||
|
const node = createMockNode({ inputs: [input] })
|
||||||
|
mockPrompt.mockResolvedValue('New Name')
|
||||||
|
|
||||||
|
const options = getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
const renameOption = options.find((o: IContextMenuValue) =>
|
||||||
|
o.content?.includes('contextMenu.RenameWidget')
|
||||||
|
)
|
||||||
|
await invokeMenuCallback(renameOption!)
|
||||||
|
|
||||||
|
expect(widget.label).toBe('New Name')
|
||||||
|
expect(input.label).toBe('New Name')
|
||||||
|
expect(widget.callback).toHaveBeenCalledWith(42)
|
||||||
|
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rename callback clears label when empty string is returned', async () => {
|
||||||
|
const widget = createMockWidget({ name: 'seed', label: 'Old' })
|
||||||
|
const input = {
|
||||||
|
widget: { name: 'seed' },
|
||||||
|
label: 'Old' as string | undefined
|
||||||
|
}
|
||||||
|
const node = createMockNode({ inputs: [input] })
|
||||||
|
mockPrompt.mockResolvedValue('')
|
||||||
|
|
||||||
|
const options = getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
const renameOption = options.find((o: IContextMenuValue) =>
|
||||||
|
o.content?.includes('contextMenu.RenameWidget')
|
||||||
|
)
|
||||||
|
await invokeMenuCallback(renameOption!)
|
||||||
|
|
||||||
|
expect(widget.label).toBeUndefined()
|
||||||
|
expect(input.label).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rename callback does nothing when prompt is cancelled', async () => {
|
||||||
|
const widget = createMockWidget({ name: 'seed', label: 'Original' })
|
||||||
|
const input = { widget: { name: 'seed' }, label: 'Original' }
|
||||||
|
const node = createMockNode({ inputs: [input] })
|
||||||
|
mockPrompt.mockResolvedValue(null)
|
||||||
|
|
||||||
|
const options = getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
const renameOption = options.find((o: IContextMenuValue) =>
|
||||||
|
o.content?.includes('contextMenu.RenameWidget')
|
||||||
|
)
|
||||||
|
await invokeMenuCallback(renameOption!)
|
||||||
|
|
||||||
|
expect(widget.label).toBe('Original')
|
||||||
|
expect(input.label).toBe('Original')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds promotion options when node is in a subgraph', async () => {
|
||||||
|
const { addWidgetPromotionOptions } = vi.mocked(
|
||||||
|
await import('@/core/graph/subgraph/promotionUtils')
|
||||||
|
)
|
||||||
|
const node = createMockNode({
|
||||||
|
graph: { isRootGraph: false }
|
||||||
|
})
|
||||||
|
const widget = createMockWidget()
|
||||||
|
|
||||||
|
getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
expect(addWidgetPromotionOptions).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not add promotion options on root graph', async () => {
|
||||||
|
const { addWidgetPromotionOptions } = vi.mocked(
|
||||||
|
await import('@/core/graph/subgraph/promotionUtils')
|
||||||
|
)
|
||||||
|
const node = createMockNode({ graph: null })
|
||||||
|
const widget = createMockWidget()
|
||||||
|
|
||||||
|
getExtraOptionsForWidget(node, widget)
|
||||||
|
|
||||||
|
expect(addWidgetPromotionOptions).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('useLitegraphService', () => {
|
||||||
|
// Lazily import to ensure mocks are in place
|
||||||
|
async function getService() {
|
||||||
|
const { useLitegraphService } =
|
||||||
|
await import('@/services/litegraphService')
|
||||||
|
return useLitegraphService()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('getCanvasCenter', () => {
|
||||||
|
it('returns center of visible area', async () => {
|
||||||
|
const service = await getService()
|
||||||
|
// visible_area = [0, 0, 800, 600], dpi = 1
|
||||||
|
const center = service.getCanvasCenter()
|
||||||
|
expect(center).toEqual([400, 300])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accounts for visible area offset', async () => {
|
||||||
|
const saved = mockCanvas.ds.visible_area
|
||||||
|
mockCanvas.ds.visible_area = [10, 20, 200, 100]
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
const center = service.getCanvasCenter()
|
||||||
|
expect(center).toEqual([110, 70])
|
||||||
|
|
||||||
|
mockCanvas.ds.visible_area = saved
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns [0, 0] when no visible area', async () => {
|
||||||
|
const savedVisibleArea = mockCanvas.ds.visible_area
|
||||||
|
mockCanvas.ds.visible_area = undefined
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
const center = service.getCanvasCenter()
|
||||||
|
expect(center).toEqual([0, 0])
|
||||||
|
|
||||||
|
mockCanvas.ds.visible_area = savedVisibleArea
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns [0, 0] without throwing when app.canvas is undefined', async () => {
|
||||||
|
mockApp.canvas = undefined
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
expect(() => service.getCanvasCenter()).not.toThrow()
|
||||||
|
expect(service.getCanvasCenter()).toEqual([0, 0])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resetView', () => {
|
||||||
|
it('resets canvas scale and offset', async () => {
|
||||||
|
mockCanvas.ds.scale = 2.5
|
||||||
|
mockCanvas.ds.offset = [100, 200]
|
||||||
|
const service = await getService()
|
||||||
|
|
||||||
|
service.resetView()
|
||||||
|
|
||||||
|
expect(mockCanvas.ds.scale).toBe(1)
|
||||||
|
expect(mockCanvas.ds.offset).toEqual([0, 0])
|
||||||
|
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('goToNode', () => {
|
||||||
|
it('animates to node bounds when node exists', async () => {
|
||||||
|
const bounds = [10, 20, 100, 50]
|
||||||
|
const graphNode = { boundingRect: bounds }
|
||||||
|
mockCanvas.graph.getNodeById.mockReturnValue(graphNode)
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
service.goToNode(42)
|
||||||
|
|
||||||
|
expect(mockCanvas.animateToBounds).toHaveBeenCalledWith(bounds)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does nothing when node does not exist', async () => {
|
||||||
|
mockCanvas.graph.getNodeById.mockReturnValue(null)
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
service.goToNode(999)
|
||||||
|
|
||||||
|
expect(mockCanvas.animateToBounds).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('fitView', () => {
|
||||||
|
it('calls fitToBounds and setDirty', async () => {
|
||||||
|
const mockBounds = [0, 0, 500, 400]
|
||||||
|
mockCreateBounds.mockReturnValue(mockBounds)
|
||||||
|
|
||||||
|
const nodeObj = {
|
||||||
|
boundingRect: [0, 0, 100, 50],
|
||||||
|
updateArea: vi.fn()
|
||||||
|
}
|
||||||
|
mockCanvas.graph.nodes = [nodeObj]
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
service.fitView()
|
||||||
|
|
||||||
|
expect(mockCanvas.ds.fitToBounds).toHaveBeenCalledWith(mockBounds)
|
||||||
|
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls updateArea for nodes with zero bounds', async () => {
|
||||||
|
mockCreateBounds.mockReturnValue([0, 0, 100, 100])
|
||||||
|
|
||||||
|
const nodeObj = {
|
||||||
|
boundingRect: [0, 0, 0, 0],
|
||||||
|
updateArea: vi.fn()
|
||||||
|
}
|
||||||
|
mockCanvas.graph.nodes = [nodeObj]
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
service.fitView()
|
||||||
|
|
||||||
|
expect(nodeObj.updateArea).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does nothing when createBounds returns null', async () => {
|
||||||
|
mockCreateBounds.mockReturnValue(null)
|
||||||
|
mockCanvas.graph.nodes = []
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
service.fitView()
|
||||||
|
|
||||||
|
expect(mockCanvas.ds.fitToBounds).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updatePreviews', () => {
|
||||||
|
it('catches errors and logs them', async () => {
|
||||||
|
const consoleSpy = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
|
||||||
|
mockNodeOutputStore.getNodeOutputs.mockImplementation(() => {
|
||||||
|
throw new Error('test error')
|
||||||
|
})
|
||||||
|
|
||||||
|
const service = await getService()
|
||||||
|
const badNode = createMockNode({ flags: { collapsed: false } })
|
||||||
|
expect(() => service.updatePreviews(badNode)).not.toThrow()
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
'Error drawing node background',
|
||||||
|
expect.any(Error)
|
||||||
|
)
|
||||||
|
|
||||||
|
consoleSpy.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips collapsed nodes', async () => {
|
||||||
|
const service = await getService()
|
||||||
|
const node = createMockNode({
|
||||||
|
flags: { collapsed: true },
|
||||||
|
imgs: undefined,
|
||||||
|
images: undefined,
|
||||||
|
preview: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
service.updatePreviews(node)
|
||||||
|
|
||||||
|
expect(mockNodeOutputStore.getNodeOutputs).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user