mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 10:00:08 +00:00
## Summary Full Integration of Asset Browsing and Selection when Assets API is enabled. ## Changes 1. Replace Model Left Side Tab with experience 2. Configurable titles for the Asset Browser Modal 3. Refactors to simplify callback code 4. Refactor to make modal filters reactive (they change their values based on assets displayed) 5. Add `browse()` mode with ability to create node directly from the Asset Browser Modal (in `browse()` mode) ## Screenshots Demo of many different types of Nodes getting configured by the Modal https://github.com/user-attachments/assets/34f9c964-cdf2-4c5d-86a9-a8e7126a7de9 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5900-Feat-asset-selection-cloud-integration-2816d73d365081ccb4aeecdc14b0e5d3) by [Unito](https://www.unito.io)
259 lines
7.9 KiB
TypeScript
259 lines
7.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
|
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
|
import { assetService } from '@/platform/assets/services/assetService'
|
|
import { useComboWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useComboWidget'
|
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
|
|
|
vi.mock('@/scripts/widgets', () => ({
|
|
addValueControlWidgets: vi.fn()
|
|
}))
|
|
|
|
const mockSettingStoreGet = vi.fn(() => false)
|
|
vi.mock('@/platform/settings/settingStore', () => ({
|
|
useSettingStore: vi.fn(() => ({
|
|
get: mockSettingStoreGet
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/i18n', () => ({
|
|
t: vi.fn((key: string) =>
|
|
key === 'widgets.selectModel' ? 'Select model' : key
|
|
)
|
|
}))
|
|
|
|
vi.mock('@/platform/assets/services/assetService', () => ({
|
|
assetService: {
|
|
isAssetBrowserEligible: vi.fn(() => false)
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/platform/assets/composables/useAssetBrowserDialog', () => {
|
|
const mockAssetBrowserDialogShow = vi.fn()
|
|
return {
|
|
useAssetBrowserDialog: vi.fn(() => ({
|
|
show: mockAssetBrowserDialogShow
|
|
}))
|
|
}
|
|
})
|
|
|
|
// Test factory functions
|
|
function createMockWidget(overrides: Partial<IBaseWidget> = {}): IBaseWidget {
|
|
const mockCallback = vi.fn()
|
|
return {
|
|
type: 'combo',
|
|
options: {},
|
|
name: 'testWidget',
|
|
value: undefined,
|
|
callback: mockCallback,
|
|
y: 0,
|
|
...overrides
|
|
} as IBaseWidget
|
|
}
|
|
|
|
function createMockNode(comfyClass = 'TestNode'): LGraphNode {
|
|
const node = new LGraphNode('TestNode')
|
|
node.comfyClass = comfyClass
|
|
|
|
// Spy on the addWidget method
|
|
vi.spyOn(node, 'addWidget').mockImplementation(
|
|
(type, name, value, callback) => {
|
|
const widget = createMockWidget({ type, name, value })
|
|
// Store the callback function on the widget for testing
|
|
if (typeof callback === 'function') {
|
|
widget.callback = callback
|
|
}
|
|
return widget
|
|
}
|
|
)
|
|
|
|
return node
|
|
}
|
|
|
|
function createMockInputSpec(overrides: Partial<InputSpec> = {}): InputSpec {
|
|
return {
|
|
type: 'COMBO',
|
|
name: 'testInput',
|
|
...overrides
|
|
} as InputSpec
|
|
}
|
|
|
|
describe('useComboWidget', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockSettingStoreGet.mockReturnValue(false)
|
|
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(false)
|
|
vi.mocked(useAssetBrowserDialog).mockClear()
|
|
})
|
|
|
|
it('should handle undefined spec', () => {
|
|
const constructor = useComboWidget()
|
|
const mockWidget = createMockWidget()
|
|
const mockNode = createMockNode()
|
|
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
|
|
const inputSpec = createMockInputSpec({ name: 'inputName' })
|
|
|
|
const widget = constructor(mockNode, inputSpec)
|
|
|
|
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
|
'combo',
|
|
'inputName',
|
|
undefined,
|
|
expect.any(Function),
|
|
expect.objectContaining({
|
|
values: []
|
|
})
|
|
)
|
|
expect(widget).toBe(mockWidget)
|
|
})
|
|
|
|
it('should create normal combo widget when asset API is disabled', () => {
|
|
mockSettingStoreGet.mockReturnValue(false) // Asset API disabled
|
|
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true) // Widget is eligible
|
|
|
|
const constructor = useComboWidget()
|
|
const mockWidget = createMockWidget()
|
|
const mockNode = createMockNode('CheckpointLoaderSimple')
|
|
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
|
|
const inputSpec = createMockInputSpec({
|
|
name: 'ckpt_name',
|
|
options: ['model1.safetensors', 'model2.safetensors']
|
|
})
|
|
|
|
const widget = constructor(mockNode, inputSpec)
|
|
expect(widget).toBe(mockWidget)
|
|
|
|
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
|
'combo',
|
|
'ckpt_name',
|
|
'model1.safetensors',
|
|
expect.any(Function),
|
|
{ values: ['model1.safetensors', 'model2.safetensors'] }
|
|
)
|
|
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
|
|
expect(widget).toBe(mockWidget)
|
|
})
|
|
|
|
it('should create asset browser widget when API enabled', () => {
|
|
mockSettingStoreGet.mockReturnValue(true)
|
|
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
|
|
|
|
const constructor = useComboWidget()
|
|
const mockWidget = createMockWidget({
|
|
type: 'asset',
|
|
name: 'ckpt_name',
|
|
value: 'model1.safetensors'
|
|
})
|
|
const mockNode = createMockNode('CheckpointLoaderSimple')
|
|
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
|
|
const inputSpec = createMockInputSpec({
|
|
name: 'ckpt_name',
|
|
options: ['model1.safetensors', 'model2.safetensors']
|
|
})
|
|
|
|
const widget = constructor(mockNode, inputSpec)
|
|
|
|
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
|
'asset',
|
|
'ckpt_name',
|
|
'model1.safetensors',
|
|
expect.any(Function)
|
|
)
|
|
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
|
|
expect(vi.mocked(assetService.isAssetBrowserEligible)).toHaveBeenCalledWith(
|
|
'CheckpointLoaderSimple'
|
|
)
|
|
expect(widget).toBe(mockWidget)
|
|
})
|
|
|
|
it('should create asset browser widget with options when API enabled', () => {
|
|
mockSettingStoreGet.mockReturnValue(true)
|
|
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
|
|
|
|
const constructor = useComboWidget()
|
|
const mockWidget = createMockWidget({
|
|
type: 'asset',
|
|
name: 'ckpt_name',
|
|
value: 'model1.safetensors'
|
|
})
|
|
const mockNode = createMockNode('CheckpointLoaderSimple')
|
|
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
|
|
const inputSpec = createMockInputSpec({
|
|
name: 'ckpt_name',
|
|
options: ['model1.safetensors', 'model2.safetensors']
|
|
})
|
|
|
|
const widget = constructor(mockNode, inputSpec)
|
|
|
|
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
|
'asset',
|
|
'ckpt_name',
|
|
'model1.safetensors',
|
|
expect.any(Function)
|
|
)
|
|
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
|
|
expect(widget).toBe(mockWidget)
|
|
})
|
|
|
|
it('should use asset browser widget even when inputSpec has a default value but no options', () => {
|
|
mockSettingStoreGet.mockReturnValue(true)
|
|
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
|
|
|
|
const constructor = useComboWidget()
|
|
const mockWidget = createMockWidget({
|
|
type: 'asset',
|
|
name: 'ckpt_name',
|
|
value: 'fallback.safetensors'
|
|
})
|
|
const mockNode = createMockNode('CheckpointLoaderSimple')
|
|
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
|
|
const inputSpec = createMockInputSpec({
|
|
name: 'ckpt_name',
|
|
default: 'fallback.safetensors'
|
|
// Note: no options array provided
|
|
})
|
|
|
|
const widget = constructor(mockNode, inputSpec)
|
|
|
|
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
|
'asset',
|
|
'ckpt_name',
|
|
'fallback.safetensors',
|
|
expect.any(Function)
|
|
)
|
|
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
|
|
expect(widget).toBe(mockWidget)
|
|
})
|
|
|
|
it('should show Select model when asset widget has undefined current value', () => {
|
|
mockSettingStoreGet.mockReturnValue(true)
|
|
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(true)
|
|
|
|
const constructor = useComboWidget()
|
|
const mockWidget = createMockWidget({
|
|
type: 'asset',
|
|
name: 'ckpt_name',
|
|
value: 'Select model'
|
|
})
|
|
const mockNode = createMockNode('CheckpointLoaderSimple')
|
|
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
|
|
const inputSpec = createMockInputSpec({
|
|
name: 'ckpt_name'
|
|
// Note: no default, no options, not remote - getDefaultValue returns undefined
|
|
})
|
|
|
|
const widget = constructor(mockNode, inputSpec)
|
|
|
|
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
|
'asset',
|
|
'ckpt_name',
|
|
'Select model', // Should fallback to this instead of undefined
|
|
expect.any(Function)
|
|
)
|
|
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
|
|
expect(widget).toBe(mockWidget)
|
|
})
|
|
})
|