mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
merge: FE-731 (PR #12375) into m1-fe-integration
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData'
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: false
|
||||
}))
|
||||
|
||||
const mockUpdateModelsForNodeType = vi.fn()
|
||||
const mockGetCategoryForNodeType = vi.fn()
|
||||
|
||||
vi.mock('@/stores/assetsStore', () => ({
|
||||
useAssetsStore: () => ({
|
||||
getAssets: () => [],
|
||||
isModelLoading: () => false,
|
||||
getError: () => undefined,
|
||||
hasAssetKey: () => false,
|
||||
updateModelsForNodeType: mockUpdateModelsForNodeType
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
useModelToNodeStore: () => ({
|
||||
getCategoryForNodeType: mockGetCategoryForNodeType
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useAssetWidgetData (desktop/isCloud=false)', () => {
|
||||
it('returns empty/default values without calling stores', () => {
|
||||
const nodeType = ref('CheckpointLoaderSimple')
|
||||
const { category, assets, isLoading, error } = useAssetWidgetData(nodeType)
|
||||
|
||||
expect(category.value).toBeUndefined()
|
||||
expect(assets.value).toEqual([])
|
||||
expect(isLoading.value).toBe(false)
|
||||
expect(error.value).toBeNull()
|
||||
expect(mockUpdateModelsForNodeType).not.toHaveBeenCalled()
|
||||
expect(mockGetCategoryForNodeType).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -4,10 +4,6 @@ import { nextTick, ref } from 'vue'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData'
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: true
|
||||
}))
|
||||
|
||||
const mockAssetsByKey = new Map<string, AssetItem[]>()
|
||||
const mockLoadingByKey = new Map<string, boolean>()
|
||||
const mockErrorByKey = new Map<string, Error | undefined>()
|
||||
@@ -31,7 +27,7 @@ vi.mock('@/stores/modelToNodeStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useAssetWidgetData (cloud mode, isCloud=true)', () => {
|
||||
describe('useAssetWidgetData', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockAssetsByKey.clear()
|
||||
|
||||
@@ -2,7 +2,6 @@ import { computed, toValue, watch } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
|
||||
@@ -11,8 +10,6 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
* Provides reactive asset data based on node type with automatic category detection.
|
||||
* Uses store-based caching to avoid duplicate fetches across multiple instances.
|
||||
*
|
||||
* Cloud-only composable - returns empty data when not in cloud environment.
|
||||
*
|
||||
* @param nodeType - ComfyUI node type (ref, getter, or plain value). Can be undefined.
|
||||
* Accepts: ref('CheckpointLoaderSimple'), () => 'CheckpointLoaderSimple', or 'CheckpointLoaderSimple'
|
||||
* @returns Reactive data including category, assets, dropdown items, loading state, and errors
|
||||
@@ -20,61 +17,52 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
export function useAssetWidgetData(
|
||||
nodeType: MaybeRefOrGetter<string | undefined>
|
||||
) {
|
||||
if (isCloud) {
|
||||
const assetsStore = useAssetsStore()
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const assetsStore = useAssetsStore()
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
|
||||
const category = computed(() => {
|
||||
const resolvedType = toValue(nodeType)
|
||||
return resolvedType
|
||||
? modelToNodeStore.getCategoryForNodeType(resolvedType)
|
||||
: undefined
|
||||
})
|
||||
const category = computed(() => {
|
||||
const resolvedType = toValue(nodeType)
|
||||
return resolvedType
|
||||
? modelToNodeStore.getCategoryForNodeType(resolvedType)
|
||||
: undefined
|
||||
})
|
||||
|
||||
const assets = computed<AssetItem[]>(() => {
|
||||
const resolvedType = toValue(nodeType)
|
||||
return resolvedType ? (assetsStore.getAssets(resolvedType) ?? []) : []
|
||||
})
|
||||
const assets = computed<AssetItem[]>(() => {
|
||||
const resolvedType = toValue(nodeType)
|
||||
return resolvedType ? (assetsStore.getAssets(resolvedType) ?? []) : []
|
||||
})
|
||||
|
||||
const isLoading = computed(() => {
|
||||
const resolvedType = toValue(nodeType)
|
||||
return resolvedType ? assetsStore.isModelLoading(resolvedType) : false
|
||||
})
|
||||
const isLoading = computed(() => {
|
||||
const resolvedType = toValue(nodeType)
|
||||
return resolvedType ? assetsStore.isModelLoading(resolvedType) : false
|
||||
})
|
||||
|
||||
const error = computed<Error | null>(() => {
|
||||
const resolvedType = toValue(nodeType)
|
||||
return resolvedType ? (assetsStore.getError(resolvedType) ?? null) : null
|
||||
})
|
||||
const error = computed<Error | null>(() => {
|
||||
const resolvedType = toValue(nodeType)
|
||||
return resolvedType ? (assetsStore.getError(resolvedType) ?? null) : null
|
||||
})
|
||||
|
||||
watch(
|
||||
() => toValue(nodeType),
|
||||
async (currentNodeType) => {
|
||||
if (!currentNodeType) {
|
||||
return
|
||||
}
|
||||
watch(
|
||||
() => toValue(nodeType),
|
||||
async (currentNodeType) => {
|
||||
if (!currentNodeType) {
|
||||
return
|
||||
}
|
||||
|
||||
const isLoading = assetsStore.isModelLoading(currentNodeType)
|
||||
const hasBeenInitialized = assetsStore.hasAssetKey(currentNodeType)
|
||||
const isLoading = assetsStore.isModelLoading(currentNodeType)
|
||||
const hasBeenInitialized = assetsStore.hasAssetKey(currentNodeType)
|
||||
|
||||
if (!isLoading && !hasBeenInitialized) {
|
||||
await assetsStore.updateModelsForNodeType(currentNodeType)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
category,
|
||||
assets,
|
||||
isLoading,
|
||||
error
|
||||
}
|
||||
}
|
||||
if (!isLoading && !hasBeenInitialized) {
|
||||
await assetsStore.updateModelsForNodeType(currentNodeType)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
category: computed(() => undefined),
|
||||
assets: computed<AssetItem[]>(() => []),
|
||||
isLoading: computed(() => false),
|
||||
error: computed(() => null)
|
||||
category,
|
||||
assets,
|
||||
isLoading,
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ function createMockAssetItem(overrides: Partial<AssetItem> = {}): AssetItem {
|
||||
}
|
||||
|
||||
// Use vi.hoisted() to ensure mock state is initialized before mocks
|
||||
const mockDistributionState = vi.hoisted(() => ({ isCloud: false }))
|
||||
const mockUpdateInputs = vi.hoisted(() => vi.fn(() => Promise.resolve()))
|
||||
const mockGetInputName = vi.hoisted(() => vi.fn((hash: string) => hash))
|
||||
const mockGetAssets = vi.hoisted(() => vi.fn(() => [] as AssetItem[]))
|
||||
@@ -41,12 +40,6 @@ vi.mock('@/scripts/widgets', () => ({
|
||||
addValueControlWidgets: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
get isCloud() {
|
||||
return mockDistributionState.isCloud
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/assetsStore', () => ({
|
||||
useAssetsStore: vi.fn(() => ({
|
||||
get inputAssets() {
|
||||
@@ -147,7 +140,6 @@ describe('useComboWidget', () => {
|
||||
vi.mocked(assetService.isAssetBrowserEligible).mockReturnValue(false)
|
||||
vi.mocked(assetService.shouldUseAssetBrowser).mockReturnValue(false)
|
||||
vi.mocked(useAssetBrowserDialog).mockClear()
|
||||
mockDistributionState.isCloud = false
|
||||
mockAssetsStoreState.inputAssets = []
|
||||
mockAssetsStoreState.inputLoading = false
|
||||
mockUpdateInputs.mockClear()
|
||||
@@ -174,8 +166,7 @@ describe('useComboWidget', () => {
|
||||
expect(widget).toBe(mockWidget)
|
||||
})
|
||||
|
||||
it('should create normal combo widget when asset API is disabled', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
it('should create normal combo widget when asset browser is not eligible', () => {
|
||||
mockSettingStoreGet.mockReturnValue(false)
|
||||
vi.mocked(assetService.shouldUseAssetBrowser).mockReturnValue(false)
|
||||
|
||||
@@ -201,15 +192,14 @@ describe('useComboWidget', () => {
|
||||
expect(widget).toBe(mockWidget)
|
||||
})
|
||||
|
||||
describe('cloud asset browser widget', () => {
|
||||
describe('asset browser widget', () => {
|
||||
// "Select model" is the fallback from t('widgets.selectModel')
|
||||
// in createAssetWidget when defaultValue is undefined.
|
||||
const PLACEHOLDER = 'Select model'
|
||||
|
||||
function setupCloudAssetWidget(
|
||||
function setupAssetBrowserWidget(
|
||||
inputSpecOverrides: Partial<InputSpec> = {}
|
||||
) {
|
||||
mockDistributionState.isCloud = true
|
||||
vi.mocked(assetService.shouldUseAssetBrowser).mockReturnValue(true)
|
||||
|
||||
const constructor = useComboWidget()
|
||||
@@ -238,7 +228,7 @@ describe('useComboWidget', () => {
|
||||
createMockAssetItem({ name: 'cloud_model.safetensors' })
|
||||
])
|
||||
|
||||
const { mockNode } = setupCloudAssetWidget({
|
||||
const { mockNode } = setupAssetBrowserWidget({
|
||||
options: ['model1.safetensors', 'model2.safetensors']
|
||||
})
|
||||
|
||||
@@ -254,38 +244,37 @@ describe('useComboWidget', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should use first cloud asset as default instead of server combo options', () => {
|
||||
it('should use first asset as default instead of server combo options', () => {
|
||||
mockGetAssets.mockReturnValue([
|
||||
createMockAssetItem({ name: 'cloud_model.safetensors' })
|
||||
createMockAssetItem({ name: 'asset_model.safetensors' })
|
||||
])
|
||||
|
||||
const { mockNode } = setupCloudAssetWidget({
|
||||
const { mockNode } = setupAssetBrowserWidget({
|
||||
options: ['local_only_model.safetensors']
|
||||
})
|
||||
|
||||
expect(getWidgetDefault(mockNode)).toBe('cloud_model.safetensors')
|
||||
expect(getWidgetDefault(mockNode)).toBe('asset_model.safetensors')
|
||||
})
|
||||
|
||||
it('should fallback to assets[0] when inputSpec.default not in cloud assets', () => {
|
||||
it('should fallback to assets[0] when inputSpec.default not in assets', () => {
|
||||
mockGetAssets.mockReturnValue([
|
||||
createMockAssetItem({ name: 'cloud_model.safetensors' })
|
||||
createMockAssetItem({ name: 'asset_model.safetensors' })
|
||||
])
|
||||
|
||||
const { mockNode } = setupCloudAssetWidget({
|
||||
default: 'not_in_cloud.safetensors'
|
||||
const { mockNode } = setupAssetBrowserWidget({
|
||||
default: 'not_in_assets.safetensors'
|
||||
})
|
||||
|
||||
expect(getWidgetDefault(mockNode)).toBe('cloud_model.safetensors')
|
||||
expect(getWidgetDefault(mockNode)).toBe('asset_model.safetensors')
|
||||
})
|
||||
|
||||
it('should prefer inputSpec.default when it exists in cloud assets', () => {
|
||||
it('should prefer inputSpec.default when it exists in assets', () => {
|
||||
mockGetAssets.mockReturnValue([
|
||||
createMockAssetItem({ name: 'other_model.safetensors' }),
|
||||
createMockAssetItem({ name: 'fallback.safetensors' })
|
||||
])
|
||||
|
||||
const { mockNode } = setupCloudAssetWidget({
|
||||
// Note: no options array provided
|
||||
const { mockNode } = setupAssetBrowserWidget({
|
||||
default: 'fallback.safetensors'
|
||||
})
|
||||
|
||||
@@ -295,18 +284,17 @@ describe('useComboWidget', () => {
|
||||
it('should create asset browser widget when default value provided without options', () => {
|
||||
mockGetAssets.mockReturnValue([])
|
||||
|
||||
const { mockNode } = setupCloudAssetWidget({
|
||||
// Note: no options array provided
|
||||
const { mockNode } = setupAssetBrowserWidget({
|
||||
default: 'fallback.safetensors'
|
||||
})
|
||||
|
||||
expect(getWidgetDefault(mockNode)).toBe(PLACEHOLDER)
|
||||
})
|
||||
|
||||
it('should fallback to placeholder when cloud assets not loaded', () => {
|
||||
it('should fallback to placeholder when assets not loaded', () => {
|
||||
mockGetAssets.mockReturnValue([])
|
||||
|
||||
const { mockNode } = setupCloudAssetWidget({
|
||||
const { mockNode } = setupAssetBrowserWidget({
|
||||
options: ['local_model.safetensors']
|
||||
})
|
||||
|
||||
@@ -315,7 +303,6 @@ describe('useComboWidget', () => {
|
||||
})
|
||||
|
||||
it('should show Select model when asset widget has undefined current value', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
vi.mocked(assetService.shouldUseAssetBrowser).mockReturnValue(true)
|
||||
|
||||
const constructor = useComboWidget()
|
||||
@@ -343,7 +330,7 @@ describe('useComboWidget', () => {
|
||||
expect(widget).toBe(mockWidget)
|
||||
})
|
||||
|
||||
describe('cloud input asset mapping', () => {
|
||||
describe('input asset mapping', () => {
|
||||
const HASH_FILENAME =
|
||||
'72e786ff2a44d682c4294db0b7098e569832bc394efc6dad644e6ec85a78efb7.png'
|
||||
const HASH_FILENAME_2 =
|
||||
@@ -354,10 +341,8 @@ describe('useComboWidget', () => {
|
||||
{ nodeClass: 'LoadVideo', inputName: 'video' },
|
||||
{ nodeClass: 'LoadAudio', inputName: 'audio' }
|
||||
])(
|
||||
'should create combo widget with getOptionLabel for $nodeClass in cloud',
|
||||
'should create combo widget with getOptionLabel for $nodeClass',
|
||||
({ nodeClass, inputName }) => {
|
||||
mockDistributionState.isCloud = true
|
||||
|
||||
const constructor = useComboWidget()
|
||||
const mockWidget = createMockWidget({
|
||||
type: 'combo',
|
||||
@@ -387,9 +372,7 @@ describe('useComboWidget', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('should keep the original options object for cloud input mappings', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
|
||||
it('should keep the original options object for input mappings', () => {
|
||||
const constructor = useComboWidget()
|
||||
const mockNode = createMockNode('LoadImage')
|
||||
const inputSpec = createMockInputSpec({
|
||||
@@ -405,7 +388,6 @@ describe('useComboWidget', () => {
|
||||
})
|
||||
|
||||
it("should format option labels using store's getInputName function", () => {
|
||||
mockDistributionState.isCloud = true
|
||||
mockGetInputName.mockReturnValue('Beautiful Sunset.png')
|
||||
|
||||
const constructor = useComboWidget()
|
||||
@@ -445,9 +427,7 @@ describe('useComboWidget', () => {
|
||||
expect(result).toBe('Beautiful Sunset.png')
|
||||
})
|
||||
|
||||
it('should create normal combo widget for non-input nodes in cloud', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
|
||||
it('should create normal combo widget for non-input nodes', () => {
|
||||
const constructor = useComboWidget()
|
||||
const mockWidget = createMockWidget()
|
||||
const mockNode = createMockNode('SomeOtherNode')
|
||||
@@ -469,34 +449,7 @@ describe('useComboWidget', () => {
|
||||
expect(widget).toBe(mockWidget)
|
||||
})
|
||||
|
||||
it('should create normal combo widget for LoadImage in OSS', () => {
|
||||
mockDistributionState.isCloud = false
|
||||
|
||||
const constructor = useComboWidget()
|
||||
const mockWidget = createMockWidget()
|
||||
const mockNode = createMockNode('LoadImage')
|
||||
vi.mocked(mockNode.addWidget).mockReturnValue(mockWidget)
|
||||
const inputSpec = createMockInputSpec({
|
||||
name: 'image',
|
||||
options: [HASH_FILENAME, HASH_FILENAME_2]
|
||||
})
|
||||
|
||||
const widget = constructor(mockNode, inputSpec)
|
||||
|
||||
expect(mockNode.addWidget).toHaveBeenCalledWith(
|
||||
'combo',
|
||||
'image',
|
||||
HASH_FILENAME,
|
||||
expect.any(Function),
|
||||
{
|
||||
values: [HASH_FILENAME, HASH_FILENAME_2]
|
||||
}
|
||||
)
|
||||
expect(widget).toBe(mockWidget)
|
||||
})
|
||||
|
||||
it('should trigger lazy load for cloud input nodes', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
it('should trigger lazy load for input nodes', () => {
|
||||
mockAssetsStoreState.inputAssets = []
|
||||
mockAssetsStoreState.inputLoading = false
|
||||
|
||||
@@ -515,7 +468,6 @@ describe('useComboWidget', () => {
|
||||
})
|
||||
|
||||
it('should not trigger lazy load if assets already loading', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
mockAssetsStoreState.inputAssets = []
|
||||
mockAssetsStoreState.inputLoading = true
|
||||
|
||||
@@ -534,7 +486,6 @@ describe('useComboWidget', () => {
|
||||
})
|
||||
|
||||
it('should not trigger lazy load if assets already loaded', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
mockAssetsStoreState.inputAssets = [
|
||||
createMockAssetItem({
|
||||
id: 'asset-123',
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { getAssetFilename } from '@/platform/assets/utils/assetMetadataUtils'
|
||||
import { createAssetWidget } from '@/platform/assets/utils/createAssetWidget'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type {
|
||||
ComboInputSpec,
|
||||
InputSpec
|
||||
@@ -106,11 +105,11 @@ const addMultiSelectWidget = (
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default value for a cloud asset widget.
|
||||
* Priority: inputSpec.default (if present in cloud assets) → first cloud
|
||||
* asset → undefined (shows placeholder).
|
||||
* Resolve the default value for an asset widget.
|
||||
* Priority: inputSpec.default (if present in assets) → first asset → undefined
|
||||
* (shows placeholder).
|
||||
*/
|
||||
function resolveCloudDefault(
|
||||
function resolveAssetDefault(
|
||||
nodeType: string,
|
||||
specDefault: string | undefined
|
||||
): string | undefined {
|
||||
@@ -119,7 +118,6 @@ function resolveCloudDefault(
|
||||
const inAssets = assets.some((a) => getAssetFilename(a) === specDefault)
|
||||
if (inAssets) return specDefault
|
||||
}
|
||||
// empty filename → undefined (shows placeholder)
|
||||
const filename = assets.length ? getAssetFilename(assets[0]) : undefined
|
||||
return filename || undefined
|
||||
}
|
||||
@@ -213,21 +211,19 @@ const addComboWidget = (
|
||||
): IBaseWidget => {
|
||||
const defaultValue = getDefaultValue(inputSpec)
|
||||
|
||||
if (isCloud) {
|
||||
if (assetService.shouldUseAssetBrowser(node.comfyClass, inputSpec.name)) {
|
||||
// Default from cloud assets, not from server combo options.
|
||||
// Server options list local files that may not exist in the user's
|
||||
// cloud asset library, leading to missing-model errors on undo/reload.
|
||||
const cloudDefault = resolveCloudDefault(
|
||||
node.comfyClass ?? '',
|
||||
inputSpec.default
|
||||
)
|
||||
return createAssetBrowserWidget(node, inputSpec, cloudDefault)
|
||||
}
|
||||
if (assetService.shouldUseAssetBrowser(node.comfyClass, inputSpec.name)) {
|
||||
// Default from asset library, not from server combo options.
|
||||
// Server options list local files that may not exist in the user's
|
||||
// asset library, leading to missing-model errors on undo/reload.
|
||||
const assetDefault = resolveAssetDefault(
|
||||
node.comfyClass ?? '',
|
||||
inputSpec.default
|
||||
)
|
||||
return createAssetBrowserWidget(node, inputSpec, assetDefault)
|
||||
}
|
||||
|
||||
if (NODE_MEDIA_TYPE_MAP[node.comfyClass ?? '']) {
|
||||
return createInputMappingWidget(node, inputSpec, defaultValue)
|
||||
}
|
||||
if (NODE_MEDIA_TYPE_MAP[node.comfyClass ?? '']) {
|
||||
return createInputMappingWidget(node, inputSpec, defaultValue)
|
||||
}
|
||||
|
||||
// Standard combo widget
|
||||
|
||||
Reference in New Issue
Block a user