diff --git a/src/components/dialog/content/LoadWorkflowWarning.vue b/src/components/dialog/content/LoadWorkflowWarning.vue index 216e54dd0..0759bb760 100644 --- a/src/components/dialog/content/LoadWorkflowWarning.vue +++ b/src/components/dialog/content/LoadWorkflowWarning.vue @@ -54,19 +54,16 @@ import Button from 'primevue/button' import ListBox from 'primevue/listbox' import { computed } from 'vue' -import { useI18n } from 'vue-i18n' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue' import { useMissingNodes } from '@/composables/nodePack/useMissingNodes' -import { useDialogService } from '@/services/dialogService' +import { useManagerHelper } from '@/composables/useManagerHelper' import { useComfyManagerStore } from '@/stores/comfyManagerStore' -import { useCommandStore } from '@/stores/commandStore' import { ManagerUIState, useManagerStateStore } from '@/stores/managerStateStore' -import { useToastStore } from '@/stores/toastStore' import type { MissingNodeType } from '@/types/comfy' import { ManagerTab } from '@/types/comfyManagerTypes' @@ -112,6 +109,7 @@ const uniqueNodes = computed(() => { }) const managerStateStore = useManagerStateStore() +const { openManager: openManagerHelper } = useManagerHelper() // Show manager buttons unless manager is disabled const showManagerButtons = computed(() => { @@ -123,35 +121,12 @@ const showInstallAllButton = computed(() => { return managerStateStore.managerUIState === ManagerUIState.NEW_UI }) +// Open manager with Missing tab for NEW_UI const openManager = async () => { - const state = managerStateStore.managerUIState - - switch (state) { - case ManagerUIState.DISABLED: - useDialogService().showSettingsDialog('extension') - break - - case ManagerUIState.LEGACY_UI: - try { - await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility') - } catch { - // If legacy command doesn't exist, show toast - const { t } = useI18n() - useToastStore().add({ - severity: 'error', - summary: t('g.error'), - detail: t('manager.legacyMenuNotAvailable'), - life: 3000 - }) - } - break - - case ManagerUIState.NEW_UI: - useDialogService().showManagerDialog({ - initialTab: ManagerTab.Missing - }) - break - } + // Use the helper with Missing tab option + await openManagerHelper({ + initialTab: ManagerTab.Missing + }) } diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue index fe1d51382..b6799f7b4 100644 --- a/src/components/helpcenter/HelpCenterMenuContent.vue +++ b/src/components/helpcenter/HelpCenterMenuContent.vue @@ -142,7 +142,7 @@ import { useI18n } from 'vue-i18n' import PuzzleIcon from '@/components/icons/PuzzleIcon.vue' import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment' -import { useDialogService } from '@/services/dialogService' +import { useManagerHelper } from '@/composables/useManagerHelper' import { type ReleaseNote } from '@/services/releaseService' import { useCommandStore } from '@/stores/commandStore' import { useReleaseStore } from '@/stores/releaseStore' @@ -191,7 +191,7 @@ const { t, locale } = useI18n() const releaseStore = useReleaseStore() const commandStore = useCommandStore() const settingStore = useSettingStore() -const dialogService = useDialogService() +const { openManager } = useManagerHelper() // Emits const emit = defineEmits<{ @@ -313,8 +313,8 @@ const menuItems = computed(() => { icon: PuzzleIcon, label: t('helpCenter.managerExtension'), showRedDot: shouldShowManagerRedDot.value, - action: () => { - dialogService.showManagerDialog() + action: async () => { + await openManager() emit('close') } }, diff --git a/src/components/topbar/CommandMenubar.vue b/src/components/topbar/CommandMenubar.vue index 56e06ac10..6a5c038af 100644 --- a/src/components/topbar/CommandMenubar.vue +++ b/src/components/topbar/CommandMenubar.vue @@ -106,13 +106,9 @@ import { useI18n } from 'vue-i18n' import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue' import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue' import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue' -import { useDialogService } from '@/services/dialogService' +import { useManagerHelper } from '@/composables/useManagerHelper' import { useCommandStore } from '@/stores/commandStore' import { useDialogStore } from '@/stores/dialogStore' -import { - ManagerUIState, - useManagerStateStore -} from '@/stores/managerStateStore' import { useMenuItemStore } from '@/stores/menuItemStore' import { useSettingStore } from '@/stores/settingStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' @@ -159,30 +155,7 @@ const showSettings = (defaultPanel?: string) => { }) } -const managerStateStore = useManagerStateStore() - -const showManageExtensions = async () => { - const state = managerStateStore.managerUIState - - switch (state) { - case ManagerUIState.DISABLED: - showSettings('extension') - break - - case ManagerUIState.LEGACY_UI: - try { - await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility') - } catch { - // If legacy command doesn't exist, fall back to extensions panel - showSettings('extension') - } - break - - case ManagerUIState.NEW_UI: - useDialogService().showManagerDialog() - break - } -} +const { openManager } = useManagerHelper() const extraMenuItems = computed(() => [ { separator: true }, @@ -207,7 +180,7 @@ const extraMenuItems = computed(() => [ key: 'manage-extensions', label: t('menu.manageExtensions'), icon: 'mdi mdi-puzzle-outline', - command: showManageExtensions + command: () => openManager() } ]) diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 2134ccdaa..b8cc56d8d 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -763,7 +763,8 @@ export function useCoreCommands(): ComfyCommand[] { detail: t('manager.legacyMenuNotAvailable'), life: 3000 }) - dialogService.showManagerDialog() + // Show settings dialog instead of new manager for legacy UI state + dialogService.showSettingsDialog('extension') } break @@ -1015,12 +1016,31 @@ export function useCoreCommands(): ComfyCommand[] { 'Comfy.Manager.CustomNodesManager.ToggleVisibility' ) } catch (error) { - useToastStore().add({ - severity: 'error', - summary: t('g.error'), - detail: t('manager.legacyMenuNotAvailable'), - life: 3000 - }) + // Check manager state to avoid infinite loop + const { ManagerUIState, useManagerStateStore } = await import( + '@/stores/managerStateStore' + ) + const managerState = useManagerStateStore().managerUIState + + if (managerState === ManagerUIState.NEW_UI) { + // Only fallback to new manager if we're in NEW_UI state + useToastStore().add({ + severity: 'error', + summary: t('g.error'), + detail: t('manager.legacyMenuNotAvailable'), + life: 3000 + }) + const managerHelper = await import('@/composables/useManagerHelper') + await managerHelper.openManager() + } else { + // In LEGACY_UI state, just show error without fallback + useToastStore().add({ + severity: 'error', + summary: t('g.error'), + detail: t('manager.legacyMenuNotAvailable'), + life: 3000 + }) + } } } }, @@ -1033,12 +1053,31 @@ export function useCoreCommands(): ComfyCommand[] { try { await useCommandStore().execute('Comfy.Manager.Menu.ToggleVisibility') } catch (error) { - useToastStore().add({ - severity: 'error', - summary: t('g.error'), - detail: t('manager.legacyMenuNotAvailable'), - life: 3000 - }) + // Check manager state to avoid infinite loop + const { ManagerUIState, useManagerStateStore } = await import( + '@/stores/managerStateStore' + ) + const managerState = useManagerStateStore().managerUIState + + if (managerState === ManagerUIState.NEW_UI) { + // Only fallback to new manager if we're in NEW_UI state + useToastStore().add({ + severity: 'error', + summary: t('g.error'), + detail: t('manager.legacyMenuNotAvailable'), + life: 3000 + }) + const managerHelper = await import('@/composables/useManagerHelper') + await managerHelper.openManager() + } else { + // In LEGACY_UI state, just show error without fallback + useToastStore().add({ + severity: 'error', + summary: t('g.error'), + detail: t('manager.legacyMenuNotAvailable'), + life: 3000 + }) + } } } }, diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index a578eb8bf..990e858db 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -1,40 +1,77 @@ -import { computed, reactive, readonly } from 'vue' +import { computed, readonly } from 'vue' -import { api } from '@/scripts/api' +import { useFeatureFlagsStore } from '@/stores/featureFlagsStore' /** - * Known server feature flags (top-level, not extensions) + * Known server feature flags */ export enum ServerFeatureFlag { + // Core features SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata', MAX_UPLOAD_SIZE = 'max_upload_size', + + // Extension features MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4' } +/** + * Known client feature flags + */ +export enum ClientFeatureFlag { + SUPPORTS_MANAGER_V4_UI = 'supports_manager_v4_ui' +} + /** * Composable for reactive access to feature flags */ export function useFeatureFlags() { - // Create reactive state that tracks server feature flags - const flags = reactive({ - get supportsPreviewMetadata() { - return api.getServerFeature(ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA) - }, - get maxUploadSize() { - return api.getServerFeature(ServerFeatureFlag.MAX_UPLOAD_SIZE) - }, - get supportsManagerV4() { - return api.getServerFeature(ServerFeatureFlag.MANAGER_SUPPORTS_V4) - } - }) + const featureFlagsStore = useFeatureFlagsStore() + + // Expose commonly used flags directly as computed properties + const flags = computed(() => ({ + // Server flags + supportsPreviewMetadata: featureFlagsStore.getServerFeature( + ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA, + false + ), + maxUploadSize: featureFlagsStore.getServerFeature( + ServerFeatureFlag.MAX_UPLOAD_SIZE + ), + supportsManagerV4: featureFlagsStore.supportsManagerV4, + + // Client flags + clientSupportsManagerV4UI: featureFlagsStore.clientSupportsManagerV4UI, + + // Store ready state + isReady: featureFlagsStore.isReady + })) // Create a reactive computed for any feature flag const featureFlag = (featurePath: string, defaultValue?: T) => { - return computed(() => api.getServerFeature(featurePath, defaultValue)) + return computed(() => + featureFlagsStore.getServerFeature(featurePath, defaultValue) + ) + } + + // Create a reactive computed for checking if server supports a feature + const serverSupportsFeature = (featurePath: string) => { + return computed(() => featureFlagsStore.serverSupportsFeature(featurePath)) } return { + // Enums for type-safe access + ServerFeatureFlag, + ClientFeatureFlag, + + // Computed flags object flags: readonly(flags), - featureFlag + + // Helper functions + featureFlag, + serverSupportsFeature, + + // Direct access to store methods (for advanced usage) + getServerFeature: featureFlagsStore.getServerFeature, + getClientFeature: featureFlagsStore.getClientFeature } } diff --git a/src/composables/useManagerHelper.ts b/src/composables/useManagerHelper.ts new file mode 100644 index 000000000..410985385 --- /dev/null +++ b/src/composables/useManagerHelper.ts @@ -0,0 +1,79 @@ +import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue' +import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue' +import { useDialogService } from '@/services/dialogService' +import { useCommandStore } from '@/stores/commandStore' +import { useDialogStore } from '@/stores/dialogStore' +import { + ManagerUIState, + useManagerStateStore +} from '@/stores/managerStateStore' +import type { ManagerTab } from '@/types/comfyManagerTypes' + +/** + * Options for opening the Manager + */ +export interface OpenManagerOptions { + /** + * Initial tab to show when opening the Manager (NEW_UI only) + */ + initialTab?: ManagerTab +} + +/** + * Opens the Manager UI based on the current state (NEW_UI, LEGACY_UI, or DISABLED) + * This is the single entry point for opening the Manager from anywhere in the app + */ +export async function openManager(options?: OpenManagerOptions): Promise { + const managerStateStore = useManagerStateStore() + const commandStore = useCommandStore() + const dialogStore = useDialogStore() + const dialogService = useDialogService() + + const state = managerStateStore.managerUIState + console.log('[Manager Helper] Opening manager with state:', state) + + switch (state) { + case ManagerUIState.DISABLED: + // Show settings dialog with extension tab + dialogStore.showDialog({ + key: 'global-settings', + headerComponent: SettingDialogHeader, + component: SettingDialogContent, + props: { + defaultPanel: 'extension' + } + }) + break + + case ManagerUIState.LEGACY_UI: + try { + // Try legacy manager command directly without causing recursion + await commandStore.execute('Comfy.Manager.Menu.ToggleVisibility') + } catch (e) { + console.warn('[Manager Helper] Legacy manager not available:', e) + // Show settings as fallback + dialogStore.showDialog({ + key: 'global-settings', + headerComponent: SettingDialogHeader, + component: SettingDialogContent, + props: { + defaultPanel: 'extension' + } + }) + } + break + + case ManagerUIState.NEW_UI: + dialogService.showManagerDialog(options) + break + } +} + +/** + * Composable for manager helper functions + */ +export function useManagerHelper() { + return { + openManager + } +} diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 857626674..dd263e6d1 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -261,6 +261,24 @@ export class ComfyApi extends EventTarget { reportedUnknownMessageTypes = new Set() + /** + * Cached feature flags store module to avoid repeated imports + */ + #featureFlagsStorePromise?: Promise< + typeof import('@/stores/featureFlagsStore') + > + + /** + * Get cached feature flags store module (lazy loading) + */ + async #getFeatureFlagsStore() { + if (!this.#featureFlagsStorePromise) { + this.#featureFlagsStorePromise = import('@/stores/featureFlagsStore') + } + const module = await this.#featureFlagsStorePromise + return module.useFeatureFlagsStore() + } + /** * Get feature flags supported by this frontend client. * Returns a copy to prevent external modification. @@ -417,13 +435,23 @@ export class ComfyApi extends EventTarget { opened = true // Send feature flags as the first message + const clientFlags = this.getClientFeatureFlags() this.socket!.send( JSON.stringify({ type: 'feature_flags', - data: this.getClientFeatureFlags() + data: clientFlags }) ) + // Update client flags in the store + this.#getFeatureFlagsStore() + .then((store) => { + store.updateClientFlags(clientFlags) + }) + .catch((error) => { + console.error('[API] Failed to update client flags:', error) + }) + if (isReconnect) { this.dispatchCustomEvent('reconnected') } @@ -444,10 +472,18 @@ export class ComfyApi extends EventTarget { if (opened) { this.dispatchCustomEvent('status', null) this.dispatchCustomEvent('reconnecting') + // Reset feature flags store when connection is lost + this.#getFeatureFlagsStore() + .then((store) => { + store.resetStore() + }) + .catch((error) => { + console.error('[API] Failed to reset feature flags store:', error) + }) } }) - this.socket.addEventListener('message', (event) => { + this.socket.addEventListener('message', async (event) => { try { if (event.data instanceof ArrayBuffer) { const view = new DataView(event.data) @@ -552,6 +588,18 @@ export class ComfyApi extends EventTarget { 'Server feature flags received:', this.serverFeatureFlags ) + // Update the reactive store asynchronously without blocking + this.#getFeatureFlagsStore() + .then((store) => { + store.updateServerFlags(msg.data) + }) + .catch((error) => { + console.error( + '[API] Failed to update feature flags store:', + error + ) + // Store update failed but api.serverFeatureFlags is still updated + }) break default: if (this.#registered.has(msg.type)) { diff --git a/src/stores/featureFlagsStore.ts b/src/stores/featureFlagsStore.ts new file mode 100644 index 000000000..2ba77f4c4 --- /dev/null +++ b/src/stores/featureFlagsStore.ts @@ -0,0 +1,139 @@ +import { get } from 'es-toolkit/compat' +import { defineStore } from 'pinia' +import { computed, nextTick, ref } from 'vue' + +/** + * Store for managing server and client feature flags reactively + */ +export const useFeatureFlagsStore = defineStore('featureFlags', () => { + // Server feature flags received from WebSocket + const serverFlags = ref>({}) + + // Client feature flags (local) + const clientFlags = ref>({}) + + // Flag to indicate if the store has received initial server flags + const isReady = ref(false) + + // Track update version to prevent race conditions + let updateVersion = 0 + + /** + * Update server feature flags + * Called when WebSocket receives feature_flags message + */ + function updateServerFlags(flags: Record) { + // Validate input + if (!flags || typeof flags !== 'object') { + console.error('[FeatureFlags] Invalid flags received:', flags) + return + } + + // Increment version for this update + const currentVersion = ++updateVersion + + // Use nextTick to batch updates and check version + void nextTick(() => { + // Only apply if this is still the latest update + if (currentVersion === updateVersion) { + serverFlags.value = { ...flags } + isReady.value = true + console.log('[FeatureFlags] Server flags updated:', serverFlags.value) + } else { + console.log( + '[FeatureFlags] Skipping outdated update, version:', + currentVersion + ) + } + }) + } + + /** + * Update client feature flags + */ + function updateClientFlags(flags: Record) { + clientFlags.value = { ...flags } + console.log('[FeatureFlags] Client flags updated:', clientFlags.value) + } + + /** + * Get a server feature flag value (reactive) + */ + function getServerFeature( + featurePath: string, + defaultValue?: T + ): T { + return get(serverFlags.value, featurePath, defaultValue) as T + } + + /** + * Check if server supports a feature (reactive) + */ + function serverSupportsFeature(featurePath: string): boolean { + return get(serverFlags.value, featurePath) === true + } + + /** + * Get a client feature flag value (reactive) + */ + function getClientFeature( + featurePath: string, + defaultValue?: T + ): T { + return get(clientFlags.value, featurePath, defaultValue) as T + } + + /** + * Computed property for manager v4 support + */ + const supportsManagerV4 = computed(() => { + return ( + getServerFeature('extension.manager.supports_v4', false) === true + ) + }) + + /** + * Computed property for client manager v4 UI support + */ + const clientSupportsManagerV4UI = computed(() => { + return getClientFeature('supports_manager_v4_ui', false) === true + }) + + /** + * Reset store state (useful for reconnection) + */ + function resetStore() { + console.log('[FeatureFlags] Resetting store state') + serverFlags.value = {} + isReady.value = false + // Note: We don't reset clientFlags as they're local + // Increment version to invalidate any pending updates + updateVersion++ + } + + /** + * Cleanup store (for unmounting or preventing memory leaks) + */ + function cleanup() { + console.log('[FeatureFlags] Cleaning up store') + resetStore() + clientFlags.value = {} + // Cancel any pending nextTick callbacks by incrementing version + updateVersion = Number.MAX_SAFE_INTEGER + } + + return { + serverFlags, + clientFlags, + isReady, + updateServerFlags, + updateClientFlags, + getServerFeature, + serverSupportsFeature, + getClientFeature, + supportsManagerV4, + clientSupportsManagerV4UI, + resetStore, + cleanup + } +}) diff --git a/src/stores/managerStateStore.ts b/src/stores/managerStateStore.ts index 2dece29a7..029226f4b 100644 --- a/src/stores/managerStateStore.ts +++ b/src/stores/managerStateStore.ts @@ -1,8 +1,8 @@ import { defineStore } from 'pinia' -import { computed, readonly } from 'vue' +import { computed, readonly, watchEffect } from 'vue' -import { api } from '@/scripts/api' import { useExtensionStore } from '@/stores/extensionStore' +import { useFeatureFlagsStore } from '@/stores/featureFlagsStore' import { useSystemStatsStore } from '@/stores/systemStatsStore' export enum ManagerUIState { @@ -14,27 +14,17 @@ export enum ManagerUIState { export const useManagerStateStore = defineStore('managerState', () => { const systemStatsStore = useSystemStatsStore() const extensionStore = useExtensionStore() + const featureFlagsStore = useFeatureFlagsStore() // Reactive computed manager state that updates when dependencies change const managerUIState = computed(() => { const systemStats = systemStatsStore.systemStats - const clientSupportsV4 = - api.getClientFeatureFlags().supports_manager_v4_ui ?? false + const clientSupportsV4 = featureFlagsStore.clientSupportsManagerV4UI const hasLegacyManager = extensionStore.extensions.some( (ext) => ext.name === 'Comfy.CustomNodesManager' ) - const serverSupportsV4 = api.getServerFeature( - 'extension.manager.supports_v4' - ) - - console.log('[Manager State Debug]', { - systemStats: systemStats?.system?.argv, - clientSupportsV4, - serverSupportsV4, - hasLegacyManager, - extensions: extensionStore.extensions.map((e) => e.name) - }) + const serverSupportsV4 = featureFlagsStore.supportsManagerV4 // Check command line args first if (systemStats?.system?.argv?.includes('--disable-manager')) { @@ -55,21 +45,44 @@ export const useManagerStateStore = defineStore('managerState', () => { return ManagerUIState.LEGACY_UI } - // No server v4 support but legacy manager extension exists = LEGACY_UI - if (hasLegacyManager) { - return ManagerUIState.LEGACY_UI - } - // If server feature flags haven't loaded yet, return DISABLED for now // This will update reactively once feature flags load - if (serverSupportsV4 === undefined) { + if (!featureFlagsStore.isReady || serverSupportsV4 === undefined) { return ManagerUIState.DISABLED } + // Server explicitly doesn't support v4 (false) = assume legacy manager exists + // OR legacy manager extension is detected + if (serverSupportsV4 === false || hasLegacyManager) { + return ManagerUIState.LEGACY_UI + } + // No manager at all = DISABLED return ManagerUIState.DISABLED }) + // Debug logging in development mode only + if (import.meta.env.DEV) { + watchEffect(() => { + const systemStats = systemStatsStore.systemStats + const clientSupportsV4 = featureFlagsStore.clientSupportsManagerV4UI + const serverSupportsV4 = featureFlagsStore.supportsManagerV4 + const hasLegacyManager = extensionStore.extensions.some( + (ext) => ext.name === 'Comfy.CustomNodesManager' + ) + + console.log('[Manager State Debug]', { + currentState: managerUIState.value, + systemStats: systemStats?.system?.argv, + clientSupportsV4, + serverSupportsV4, + hasLegacyManager, + isReady: featureFlagsStore.isReady, + extensions: extensionStore.extensions.map((e) => e.name) + }) + }) + } + return { managerUIState: readonly(managerUIState) } diff --git a/tests-ui/tests/composables/useFeatureFlags.test.ts b/tests-ui/tests/composables/useFeatureFlags.test.ts index eddb57b65..8ff0979f7 100644 --- a/tests-ui/tests/composables/useFeatureFlags.test.ts +++ b/tests-ui/tests/composables/useFeatureFlags.test.ts @@ -1,3 +1,4 @@ +import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { isReactive, isReadonly } from 'vue' @@ -5,18 +6,31 @@ import { ServerFeatureFlag, useFeatureFlags } from '@/composables/useFeatureFlags' -import { api } from '@/scripts/api' +import { useFeatureFlagsStore } from '@/stores/featureFlagsStore' -// Mock the API module -vi.mock('@/scripts/api', () => ({ - api: { - getServerFeature: vi.fn() - } +// Mock the store module +vi.mock('@/stores/featureFlagsStore', () => ({ + useFeatureFlagsStore: vi.fn() })) describe('useFeatureFlags', () => { + let mockStore: any + beforeEach(() => { vi.clearAllMocks() + setActivePinia(createPinia()) + + // Create mock store + mockStore = { + getServerFeature: vi.fn(), + getClientFeature: vi.fn(), + serverSupportsFeature: vi.fn(), + supportsManagerV4: false, + clientSupportsManagerV4UI: false, + isReady: false + } + + vi.mocked(useFeatureFlagsStore).mockReturnValue(mockStore) }) describe('flags object', () => { @@ -28,69 +42,74 @@ describe('useFeatureFlags', () => { }) it('should access supportsPreviewMetadata', () => { - vi.mocked(api.getServerFeature).mockImplementation( - (path, defaultValue) => { - if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA) - return true as any + mockStore.getServerFeature.mockImplementation( + (path: string, defaultValue?: any) => { + if (path === ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA) return true return defaultValue } ) const { flags } = useFeatureFlags() - expect(flags.supportsPreviewMetadata).toBe(true) - expect(api.getServerFeature).toHaveBeenCalledWith( - ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA + expect(flags.value.supportsPreviewMetadata).toBe(true) + expect(mockStore.getServerFeature).toHaveBeenCalledWith( + ServerFeatureFlag.SUPPORTS_PREVIEW_METADATA, + false ) }) it('should access maxUploadSize', () => { - vi.mocked(api.getServerFeature).mockImplementation( - (path, defaultValue) => { - if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) - return 209715200 as any // 200MB + mockStore.getServerFeature.mockImplementation( + (path: string, defaultValue?: any) => { + if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) return 209715200 // 200MB return defaultValue } ) const { flags } = useFeatureFlags() - expect(flags.maxUploadSize).toBe(209715200) - expect(api.getServerFeature).toHaveBeenCalledWith( + expect(flags.value.maxUploadSize).toBe(209715200) + expect(mockStore.getServerFeature).toHaveBeenCalledWith( ServerFeatureFlag.MAX_UPLOAD_SIZE ) }) it('should access supportsManagerV4', () => { - vi.mocked(api.getServerFeature).mockImplementation( - (path, defaultValue) => { - if (path === ServerFeatureFlag.MANAGER_SUPPORTS_V4) return true as any - return defaultValue - } - ) + mockStore.supportsManagerV4 = true const { flags } = useFeatureFlags() - expect(flags.supportsManagerV4).toBe(true) - expect(api.getServerFeature).toHaveBeenCalledWith( - ServerFeatureFlag.MANAGER_SUPPORTS_V4 - ) + expect(flags.value.supportsManagerV4).toBe(true) }) - it('should return undefined when features are not available and no default provided', () => { - vi.mocked(api.getServerFeature).mockImplementation( - (_path, defaultValue) => defaultValue as any + it('should access clientSupportsManagerV4UI', () => { + mockStore.clientSupportsManagerV4UI = true + + const { flags } = useFeatureFlags() + expect(flags.value.clientSupportsManagerV4UI).toBe(true) + }) + + it('should access isReady state', () => { + mockStore.isReady = true + + const { flags } = useFeatureFlags() + expect(flags.value.isReady).toBe(true) + }) + + it('should return default values when features are not available', () => { + mockStore.getServerFeature.mockImplementation( + (_path: string, defaultValue?: any) => defaultValue ) const { flags } = useFeatureFlags() - expect(flags.supportsPreviewMetadata).toBeUndefined() - expect(flags.maxUploadSize).toBeUndefined() - expect(flags.supportsManagerV4).toBeUndefined() + expect(flags.value.supportsPreviewMetadata).toBe(false) // default value is false + expect(flags.value.maxUploadSize).toBeUndefined() + expect(flags.value.supportsManagerV4).toBe(false) // store mock returns false }) }) describe('featureFlag', () => { it('should create reactive computed for custom feature flags', () => { - vi.mocked(api.getServerFeature).mockImplementation( - (path, defaultValue) => { - if (path === 'custom.feature') return 'custom-value' as any + mockStore.getServerFeature.mockImplementation( + (path: string, defaultValue?: any) => { + if (path === 'custom.feature') return 'custom-value' return defaultValue } ) @@ -99,16 +118,16 @@ describe('useFeatureFlags', () => { const customFlag = featureFlag('custom.feature', 'default') expect(customFlag.value).toBe('custom-value') - expect(api.getServerFeature).toHaveBeenCalledWith( + expect(mockStore.getServerFeature).toHaveBeenCalledWith( 'custom.feature', 'default' ) }) it('should handle nested paths', () => { - vi.mocked(api.getServerFeature).mockImplementation( - (path, defaultValue) => { - if (path === 'extension.custom.nested.feature') return true as any + mockStore.getServerFeature.mockImplementation( + (path: string, defaultValue?: any) => { + if (path === 'extension.custom.nested.feature') return true return defaultValue } ) @@ -120,10 +139,9 @@ describe('useFeatureFlags', () => { }) it('should work with ServerFeatureFlag enum', () => { - vi.mocked(api.getServerFeature).mockImplementation( - (path, defaultValue) => { - if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) - return 104857600 as any + mockStore.getServerFeature.mockImplementation( + (path: string, defaultValue?: any) => { + if (path === ServerFeatureFlag.MAX_UPLOAD_SIZE) return 104857600 return defaultValue } ) @@ -134,4 +152,32 @@ describe('useFeatureFlags', () => { expect(maxUploadSize.value).toBe(104857600) }) }) + + describe('serverSupportsFeature', () => { + it('should create reactive computed for feature support check', () => { + mockStore.serverSupportsFeature.mockImplementation( + (path: string) => path === 'supported.feature' + ) + + const { serverSupportsFeature } = useFeatureFlags() + const isSupported = serverSupportsFeature('supported.feature') + + expect(isSupported.value).toBe(true) + expect(mockStore.serverSupportsFeature).toHaveBeenCalledWith( + 'supported.feature' + ) + }) + }) + + describe('direct store methods', () => { + it('should expose getServerFeature method', () => { + const { getServerFeature } = useFeatureFlags() + expect(getServerFeature).toBe(mockStore.getServerFeature) + }) + + it('should expose getClientFeature method', () => { + const { getClientFeature } = useFeatureFlags() + expect(getClientFeature).toBe(mockStore.getClientFeature) + }) + }) }) diff --git a/tests-ui/tests/stores/managerStateStore.test.ts b/tests-ui/tests/stores/managerStateStore.test.ts index 28047b12a..c55670328 100644 --- a/tests-ui/tests/stores/managerStateStore.test.ts +++ b/tests-ui/tests/stores/managerStateStore.test.ts @@ -1,9 +1,8 @@ import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { useFeatureFlags } from '@/composables/useFeatureFlags' -import { api } from '@/scripts/api' import { useExtensionStore } from '@/stores/extensionStore' +import { useFeatureFlagsStore } from '@/stores/featureFlagsStore' import { ManagerUIState, useManagerStateStore @@ -11,18 +10,8 @@ import { import { useSystemStatsStore } from '@/stores/systemStatsStore' // Mock dependencies -vi.mock('@/scripts/api', () => ({ - api: { - getClientFeatureFlags: vi.fn(), - getServerFeature: vi.fn() - } -})) - -vi.mock('@/composables/useFeatureFlags', () => ({ - useFeatureFlags: vi.fn(() => ({ - flags: { supportsManagerV4: false }, - featureFlag: vi.fn() - })) +vi.mock('@/stores/featureFlagsStore', () => ({ + useFeatureFlagsStore: vi.fn() })) vi.mock('@/stores/extensionStore', () => ({ @@ -46,7 +35,11 @@ describe('useManagerStateStore', () => { system: { argv: ['python', 'main.py', '--disable-manager'] } } } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(useFeatureFlagsStore).mockReturnValue({ + clientSupportsManagerV4UI: false, + supportsManagerV4: false, + isReady: false + } as any) vi.mocked(useExtensionStore).mockReturnValue({ extensions: [] } as any) @@ -62,7 +55,11 @@ describe('useManagerStateStore', () => { system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] } } } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) + vi.mocked(useFeatureFlagsStore).mockReturnValue({ + clientSupportsManagerV4UI: false, + supportsManagerV4: false, + isReady: false + } as any) vi.mocked(useExtensionStore).mockReturnValue({ extensions: [] } as any) @@ -76,13 +73,10 @@ describe('useManagerStateStore', () => { vi.mocked(useSystemStatsStore).mockReturnValue({ systemStats: { system: { argv: ['python', 'main.py'] } } } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({ - supports_manager_v4_ui: true - }) - vi.mocked(api.getServerFeature).mockReturnValue(true) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: true }, - featureFlag: vi.fn() + vi.mocked(useFeatureFlagsStore).mockReturnValue({ + clientSupportsManagerV4UI: true, + supportsManagerV4: true, + isReady: true } as any) vi.mocked(useExtensionStore).mockReturnValue({ extensions: [] @@ -97,13 +91,10 @@ describe('useManagerStateStore', () => { vi.mocked(useSystemStatsStore).mockReturnValue({ systemStats: { system: { argv: ['python', 'main.py'] } } } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({ - supports_manager_v4_ui: false - }) - vi.mocked(api.getServerFeature).mockReturnValue(true) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: true }, - featureFlag: vi.fn() + vi.mocked(useFeatureFlagsStore).mockReturnValue({ + clientSupportsManagerV4UI: false, + supportsManagerV4: true, + isReady: true } as any) vi.mocked(useExtensionStore).mockReturnValue({ extensions: [] @@ -118,10 +109,10 @@ describe('useManagerStateStore', () => { vi.mocked(useSystemStatsStore).mockReturnValue({ systemStats: { system: { argv: ['python', 'main.py'] } } } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: false }, - featureFlag: vi.fn() + vi.mocked(useFeatureFlagsStore).mockReturnValue({ + clientSupportsManagerV4UI: false, + supportsManagerV4: false, + isReady: true } as any) vi.mocked(useExtensionStore).mockReturnValue({ extensions: [{ name: 'Comfy.CustomNodesManager' }] @@ -136,11 +127,10 @@ describe('useManagerStateStore', () => { vi.mocked(useSystemStatsStore).mockReturnValue({ systemStats: { system: { argv: ['python', 'main.py'] } } } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) - vi.mocked(api.getServerFeature).mockReturnValue(undefined) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: undefined }, - featureFlag: vi.fn() + vi.mocked(useFeatureFlagsStore).mockReturnValue({ + clientSupportsManagerV4UI: false, + supportsManagerV4: undefined, + isReady: true } as any) vi.mocked(useExtensionStore).mockReturnValue({ extensions: [] @@ -155,11 +145,10 @@ describe('useManagerStateStore', () => { vi.mocked(useSystemStatsStore).mockReturnValue({ systemStats: { system: { argv: ['python', 'main.py'] } } } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({}) - vi.mocked(api.getServerFeature).mockReturnValue(false) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: false }, - featureFlag: vi.fn() + vi.mocked(useFeatureFlagsStore).mockReturnValue({ + clientSupportsManagerV4UI: false, + supportsManagerV4: false, + isReady: true } as any) vi.mocked(useExtensionStore).mockReturnValue({ extensions: [] @@ -174,13 +163,10 @@ describe('useManagerStateStore', () => { vi.mocked(useSystemStatsStore).mockReturnValue({ systemStats: null } as any) - vi.mocked(api.getClientFeatureFlags).mockReturnValue({ - supports_manager_v4_ui: true - }) - vi.mocked(api.getServerFeature).mockReturnValue(true) - vi.mocked(useFeatureFlags).mockReturnValue({ - flags: { supportsManagerV4: true }, - featureFlag: vi.fn() + vi.mocked(useFeatureFlagsStore).mockReturnValue({ + clientSupportsManagerV4UI: true, + supportsManagerV4: true, + isReady: true } as any) vi.mocked(useExtensionStore).mockReturnValue({ extensions: []