From 54b7fa3b3192a60b40928966d95256eb1d228f82 Mon Sep 17 00:00:00 2001 From: shivansh-gupta4 Date: Sun, 6 Jul 2025 13:34:40 +0530 Subject: [PATCH 01/12] Feature Implemented: Warning displayed when frontend version mismatches --- .../dialog/content/VersionMismatchWarning.vue | 80 ++++++ src/constants/coreSettings.ts | 6 + src/locales/en/main.json | 7 + src/schemas/apiSchema.ts | 3 + src/stores/README.md | 1 + src/stores/versionCompatibilityStore.ts | 125 ++++++++ src/views/GraphView.vue | 7 + tests-ui/tests/store/systemStatsStore.test.ts | 27 ++ .../store/versionCompatibilityStore.test.ts | 267 ++++++++++++++++++ 9 files changed, 523 insertions(+) create mode 100644 src/components/dialog/content/VersionMismatchWarning.vue create mode 100644 src/stores/versionCompatibilityStore.ts create mode 100644 tests-ui/tests/store/versionCompatibilityStore.test.ts diff --git a/src/components/dialog/content/VersionMismatchWarning.vue b/src/components/dialog/content/VersionMismatchWarning.vue new file mode 100644 index 0000000000..eaf2c2a573 --- /dev/null +++ b/src/components/dialog/content/VersionMismatchWarning.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index b01b6ab0e3..9720208a72 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -873,5 +873,11 @@ export const CORE_SETTINGS: SettingParams[] = [ name: 'Release seen timestamp', type: 'hidden', defaultValue: 0 + }, + { + id: 'Comfy.VersionMismatch.DismissedVersion', + name: 'Dismissed version mismatch warning', + type: 'hidden', + defaultValue: '' } ] diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 92b48f7032..3765dc2856 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1233,6 +1233,13 @@ "outdatedVersionGeneric": "Some nodes require a newer version of ComfyUI. Please update to use all nodes.", "coreNodesFromVersion": "Requires ComfyUI {version}:" }, + "versionMismatchWarning": { + "title": "Version Compatibility Warning", + "frontendOutdated": "Frontend version {frontendVersion} is outdated. Backend requires version {requiredVersion} or higher.", + "frontendNewer": "Frontend version {frontendVersion} may not be compatible with backend version {backendVersion}.", + "updateFrontend": "Update Frontend", + "dismiss": "Dismiss" + }, "errorDialog": { "defaultTitle": "An error occurred", "loadWorkflowTitle": "Loading aborted due to error reloading workflow data", diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index db55f61f0e..8fe9901038 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -317,6 +317,7 @@ export const zSystemStats = z.object({ embedded_python: z.boolean(), comfyui_version: z.string(), pytorch_version: z.string(), + required_frontend_version: z.string().optional(), argv: z.array(z.string()), ram_total: z.number(), ram_free: z.number() @@ -481,6 +482,8 @@ const zSettings = z.object({ "what's new seen" ]), 'Comfy.Release.Timestamp': z.number(), + /** Version compatibility settings */ + 'Comfy.VersionMismatch.DismissedVersion': z.string(), /** Settings used for testing */ 'test.setting': z.any(), 'main.sub.setting.name': z.any(), diff --git a/src/stores/README.md b/src/stores/README.md index 3a61d7ae65..de45fdc71a 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -135,6 +135,7 @@ The following table lists ALL stores in the system as of 2025-01-30: | toastStore.ts | Manages toast notifications | UI | | userFileStore.ts | Manages user file operations | Files | | userStore.ts | Manages user data and preferences | User | +| versionCompatibilityStore.ts | Manages frontend/backend version compatibility warnings | Core | | widgetStore.ts | Manages widget configurations | Widgets | | workflowStore.ts | Handles workflow data and operations | Workflows | | workflowTemplatesStore.ts | Manages workflow templates | Workflows | diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts new file mode 100644 index 0000000000..c57cd886cd --- /dev/null +++ b/src/stores/versionCompatibilityStore.ts @@ -0,0 +1,125 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +import config from '@/config' +import { useSettingStore } from '@/stores/settingStore' +import { useSystemStatsStore } from '@/stores/systemStatsStore' +import { compareVersions } from '@/utils/formatUtil' + +export const useVersionCompatibilityStore = defineStore( + 'versionCompatibility', + () => { + const systemStatsStore = useSystemStatsStore() + const settingStore = useSettingStore() + + const isDismissed = ref(false) + const dismissedVersion = ref(null) + + const frontendVersion = computed(() => config.app_version) + const backendVersion = computed( + () => systemStatsStore.systemStats?.system?.comfyui_version ?? '' + ) + const requiredFrontendVersion = computed( + () => + systemStatsStore.systemStats?.system?.required_frontend_version ?? '' + ) + + const isFrontendOutdated = computed(() => { + if (!frontendVersion.value || !requiredFrontendVersion.value) { + return false + } + return ( + compareVersions(requiredFrontendVersion.value, frontendVersion.value) > + 0 + ) + }) + + const isFrontendNewer = computed(() => { + if (!frontendVersion.value || !backendVersion.value) { + return false + } + const versionDiff = compareVersions( + frontendVersion.value, + backendVersion.value + ) + return versionDiff > 0 + }) + + const hasVersionMismatch = computed(() => { + return isFrontendOutdated.value || isFrontendNewer.value + }) + + const shouldShowWarning = computed(() => { + if (!hasVersionMismatch.value || isDismissed.value) { + return false + } + + const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` + return dismissedVersion.value !== currentVersionKey + }) + + const warningMessage = computed(() => { + if (isFrontendOutdated.value) { + return { + type: 'outdated' as const, + frontendVersion: frontendVersion.value, + requiredVersion: requiredFrontendVersion.value + } + } else if (isFrontendNewer.value) { + return { + type: 'newer' as const, + frontendVersion: frontendVersion.value, + backendVersion: backendVersion.value + } + } + return null + }) + + async function checkVersionCompatibility() { + if (!systemStatsStore.systemStats) { + await systemStatsStore.fetchSystemStats() + } + } + + async function dismissWarning() { + isDismissed.value = true + const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` + dismissedVersion.value = currentVersionKey + + await settingStore.set( + 'Comfy.VersionMismatch.DismissedVersion', + currentVersionKey + ) + } + + function restoreDismissalState() { + const dismissed = settingStore.get( + 'Comfy.VersionMismatch.DismissedVersion' + ) + if (dismissed) { + dismissedVersion.value = dismissed + const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` + isDismissed.value = dismissed === currentVersionKey + } + } + + async function initialize() { + await checkVersionCompatibility() + restoreDismissalState() + } + + return { + frontendVersion, + backendVersion, + requiredFrontendVersion, + hasVersionMismatch, + shouldShowWarning, + warningMessage, + isFrontendOutdated, + isFrontendNewer, + checkVersionCompatibility, + dismissWarning, + initialize + } + } +) diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index 5d6719a488..cdf91028e7 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -9,6 +9,7 @@
+
@@ -28,6 +29,7 @@ import { useI18n } from 'vue-i18n' import MenuHamburger from '@/components/MenuHamburger.vue' import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue' +import VersionMismatchWarning from '@/components/dialog/content/VersionMismatchWarning.vue' import GraphCanvas from '@/components/graph/GraphCanvas.vue' import GlobalToast from '@/components/toast/GlobalToast.vue' import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue' @@ -54,6 +56,7 @@ import { } from '@/stores/queueStore' import { useServerConfigStore } from '@/stores/serverConfigStore' import { useSettingStore } from '@/stores/settingStore' +import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore' import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' @@ -70,6 +73,7 @@ const settingStore = useSettingStore() const executionStore = useExecutionStore() const colorPaletteStore = useColorPaletteStore() const queueStore = useQueueStore() +const versionCompatibilityStore = useVersionCompatibilityStore() watch( () => colorPaletteStore.completedActivePalette, @@ -206,6 +210,9 @@ onMounted(() => { } catch (e) { console.error('Failed to init ComfyUI frontend', e) } + + // Initialize version compatibility checking (fire-and-forget) + void versionCompatibilityStore.initialize() }) onBeforeUnmount(() => { diff --git a/tests-ui/tests/store/systemStatsStore.test.ts b/tests-ui/tests/store/systemStatsStore.test.ts index 3376a19c07..84e84ec44a 100644 --- a/tests-ui/tests/store/systemStatsStore.test.ts +++ b/tests-ui/tests/store/systemStatsStore.test.ts @@ -41,6 +41,7 @@ describe('useSystemStatsStore', () => { embedded_python: false, comfyui_version: '1.0.0', pytorch_version: '2.0.0', + required_frontend_version: '1.24.0', argv: [], ram_total: 16000000000, ram_free: 8000000000 @@ -92,6 +93,32 @@ describe('useSystemStatsStore', () => { expect(store.isLoading).toBe(false) }) + + it('should handle system stats updates', async () => { + const updatedStats = { + system: { + os: 'Windows', + python_version: '3.11.0', + embedded_python: false, + comfyui_version: '1.1.0', + pytorch_version: '2.1.0', + required_frontend_version: '1.25.0', + argv: [], + ram_total: 16000000000, + ram_free: 7000000000 + }, + devices: [] + } + + vi.mocked(api.getSystemStats).mockResolvedValue(updatedStats) + + await store.fetchSystemStats() + + expect(store.systemStats).toEqual(updatedStats) + expect(store.isLoading).toBe(false) + expect(store.error).toBeNull() + expect(api.getSystemStats).toHaveBeenCalled() + }) }) describe('getFormFactor', () => { diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts new file mode 100644 index 0000000000..d044bfb023 --- /dev/null +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -0,0 +1,267 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useSettingStore } from '@/stores/settingStore' +import { useSystemStatsStore } from '@/stores/systemStatsStore' +import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore' + +vi.mock('@/config', () => ({ + default: { + app_version: '1.24.0' + } +})) + +vi.mock('@/stores/systemStatsStore') +vi.mock('@/stores/settingStore') + +describe('useVersionCompatibilityStore', () => { + let store: ReturnType + let mockSystemStatsStore: any + let mockSettingStore: any + + beforeEach(() => { + setActivePinia(createPinia()) + + mockSystemStatsStore = { + systemStats: null, + fetchSystemStats: vi.fn() + } + + mockSettingStore = { + get: vi.fn(), + set: vi.fn() + } + + vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore) + vi.mocked(useSettingStore).mockReturnValue(mockSettingStore) + + store = useVersionCompatibilityStore() + }) + + describe('version compatibility detection', () => { + it('should detect frontend is outdated when required version is higher', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(true) + expect(store.isFrontendNewer).toBe(false) + expect(store.hasVersionMismatch).toBe(true) + }) + + it('should detect frontend is newer when frontend version is higher than backend', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.23.0', + required_frontend_version: '1.23.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(true) + expect(store.hasVersionMismatch).toBe(true) + }) + + it('should not detect mismatch when versions are compatible', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.24.0', + required_frontend_version: '1.24.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(false) + expect(store.hasVersionMismatch).toBe(false) + }) + + it('should handle missing version information gracefully', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '', + required_frontend_version: '' + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(false) + expect(store.hasVersionMismatch).toBe(false) + }) + }) + + describe('warning display logic', () => { + beforeEach(() => { + mockSettingStore.get.mockReturnValue('') + }) + + it('should show warning when there is a version mismatch and not dismissed', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.shouldShowWarning).toBe(true) + }) + + it('should not show warning when dismissed', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + void store.dismissWarning() + + expect(store.shouldShowWarning).toBe(false) + }) + + it('should not show warning when no version mismatch', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.24.0', + required_frontend_version: '1.24.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.shouldShowWarning).toBe(false) + }) + }) + + describe('warning messages', () => { + it('should generate outdated message when frontend is outdated', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.warningMessage).toEqual({ + type: 'outdated', + frontendVersion: '1.24.0', + requiredVersion: '1.25.0' + }) + }) + + it('should generate newer message when frontend is newer', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.23.0', + required_frontend_version: '1.23.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.warningMessage).toEqual({ + type: 'newer', + frontendVersion: '1.24.0', + backendVersion: '1.23.0' + }) + }) + + it('should return null when no mismatch', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.24.0', + required_frontend_version: '1.24.0' + } + } + + await store.checkVersionCompatibility() + + expect(store.warningMessage).toBeNull() + }) + }) + + describe('dismissal persistence', () => { + it('should save dismissal to settings', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.checkVersionCompatibility() + await store.dismissWarning() + + expect(mockSettingStore.set).toHaveBeenCalledWith( + 'Comfy.VersionMismatch.DismissedVersion', + '1.24.0-1.25.0-1.25.0' + ) + }) + + it('should restore dismissal state from settings', async () => { + mockSettingStore.get.mockReturnValue('1.24.0-1.25.0-1.25.0') + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.initialize() + + expect(store.shouldShowWarning).toBe(false) + }) + + it('should show warning for different version combinations even if previous was dismissed', async () => { + mockSettingStore.get.mockReturnValue('1.24.0-1.25.0-1.25.0') + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.26.0', + required_frontend_version: '1.26.0' + } + } + + await store.initialize() + + expect(store.shouldShowWarning).toBe(true) + }) + }) + + describe('initialization', () => { + it('should fetch system stats if not available', async () => { + mockSystemStatsStore.systemStats = null + + await store.initialize() + + expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled() + }) + + it('should not fetch system stats if already available', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.24.0', + required_frontend_version: '1.24.0' + } + } + + await store.initialize() + + expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled() + }) + }) +}) From e1316214637043b3a974407ccf4c61b6b26e259a Mon Sep 17 00:00:00 2001 From: shivansh-gupta4 Date: Sat, 12 Jul 2025 15:42:01 +0530 Subject: [PATCH 02/12] Created a computed for generating current_version_key and added semver validation --- .../dialog/content/VersionMismatchWarning.vue | 9 +++-- src/stores/versionCompatibilityStore.ts | 33 ++++++++++++------- .../store/versionCompatibilityStore.test.ts | 15 +++++++++ 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/components/dialog/content/VersionMismatchWarning.vue b/src/components/dialog/content/VersionMismatchWarning.vue index eaf2c2a573..0465e6dca7 100644 --- a/src/components/dialog/content/VersionMismatchWarning.vue +++ b/src/components/dialog/content/VersionMismatchWarning.vue @@ -71,10 +71,9 @@ const handleDismiss = () => { const handleUpdate = () => { // Open ComfyUI documentation or update instructions - window.open('https://docs.comfy.org/get_started/introduction', '_blank') + window.open( + 'https://docs.comfy.org/installation/update_comfyui#missing-or-outdated-frontend%2C-workflow-templates%2C-node-after-updates', + '_blank' + ) } - - diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts index c57cd886cd..236d31b9ff 100644 --- a/src/stores/versionCompatibilityStore.ts +++ b/src/stores/versionCompatibilityStore.ts @@ -4,7 +4,7 @@ import { computed, ref } from 'vue' import config from '@/config' import { useSettingStore } from '@/stores/settingStore' import { useSystemStatsStore } from '@/stores/systemStatsStore' -import { compareVersions } from '@/utils/formatUtil' +import { compareVersions, isSemVer } from '@/utils/formatUtil' export const useVersionCompatibilityStore = defineStore( 'versionCompatibility', @@ -25,7 +25,12 @@ export const useVersionCompatibilityStore = defineStore( ) const isFrontendOutdated = computed(() => { - if (!frontendVersion.value || !requiredFrontendVersion.value) { + if ( + !frontendVersion.value || + !requiredFrontendVersion.value || + !isSemVer(frontendVersion.value) || + !isSemVer(requiredFrontendVersion.value) + ) { return false } return ( @@ -35,7 +40,12 @@ export const useVersionCompatibilityStore = defineStore( }) const isFrontendNewer = computed(() => { - if (!frontendVersion.value || !backendVersion.value) { + if ( + !frontendVersion.value || + !backendVersion.value || + !isSemVer(frontendVersion.value) || + !isSemVer(backendVersion.value) + ) { return false } const versionDiff = compareVersions( @@ -49,13 +59,16 @@ export const useVersionCompatibilityStore = defineStore( return isFrontendOutdated.value || isFrontendNewer.value }) + const currentVersionKey = computed( + () => + `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` + ) + const shouldShowWarning = computed(() => { if (!hasVersionMismatch.value || isDismissed.value) { return false } - - const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` - return dismissedVersion.value !== currentVersionKey + return dismissedVersion.value !== currentVersionKey.value }) const warningMessage = computed(() => { @@ -83,12 +96,11 @@ export const useVersionCompatibilityStore = defineStore( async function dismissWarning() { isDismissed.value = true - const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` - dismissedVersion.value = currentVersionKey + dismissedVersion.value = currentVersionKey.value await settingStore.set( 'Comfy.VersionMismatch.DismissedVersion', - currentVersionKey + currentVersionKey.value ) } @@ -98,8 +110,7 @@ export const useVersionCompatibilityStore = defineStore( ) if (dismissed) { dismissedVersion.value = dismissed - const currentVersionKey = `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` - isDismissed.value = dismissed === currentVersionKey + isDismissed.value = dismissed === currentVersionKey.value } } diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts index d044bfb023..4f72f62305 100644 --- a/tests-ui/tests/store/versionCompatibilityStore.test.ts +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -98,6 +98,21 @@ describe('useVersionCompatibilityStore', () => { expect(store.isFrontendNewer).toBe(false) expect(store.hasVersionMismatch).toBe(false) }) + + it('should not detect mismatch when versions are not valid semver', async () => { + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be', // git hash + required_frontend_version: '1.29.2.45' // invalid semver + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(false) + expect(store.hasVersionMismatch).toBe(false) + }) }) describe('warning display logic', () => { From 4f336f7ac3c1323058d238183184f9a61375148f Mon Sep 17 00:00:00 2001 From: SHIVANSH GUPTA <121501003+shivansh-gupta4@users.noreply.github.com> Date: Sun, 20 Jul 2025 11:48:54 +0530 Subject: [PATCH 03/12] Corrected the Eslint and Prettier errors --- src/views/GraphView.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index fafd1d3f9d..73dea2212a 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -82,7 +82,6 @@ const showBottomMenu = computed( () => !isMobile.value && useNewMenu.value === 'Bottom' ) - watch( () => colorPaletteStore.completedActivePalette, (newTheme) => { From 561bb008507b727aa94710a736b4029895cb5fd1 Mon Sep 17 00:00:00 2001 From: shivansh-gupta4 Date: Mon, 21 Jul 2025 00:58:56 +0530 Subject: [PATCH 04/12] Corrected persisting logic in versioncompatibility store, corrected placement of component in graphview.vue so that it only loads after the versions are fetched from api and corrected the UI of the banner to be more smooth with better UX --- .../dialog/content/VersionMismatchWarning.vue | 13 ++++++++++--- src/stores/versionCompatibilityStore.ts | 4 +++- src/views/GraphView.vue | 4 +--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/dialog/content/VersionMismatchWarning.vue b/src/components/dialog/content/VersionMismatchWarning.vue index 0465e6dca7..4efcaf4df3 100644 --- a/src/components/dialog/content/VersionMismatchWarning.vue +++ b/src/components/dialog/content/VersionMismatchWarning.vue @@ -3,11 +3,11 @@ v-if="versionStore.shouldShowWarning" severity="warn" icon="pi pi-exclamation-triangle" - class="my-2 mx-2" - :closable="true" + class="my-2 mx-2 version-warning-fix" :pt="{ root: { class: 'flex-col' }, - text: { class: 'flex-1' } + text: { class: 'flex-1' }, + icon: { class: 'flex items-start mt-1' } }" @close="handleDismiss" > @@ -77,3 +77,10 @@ const handleUpdate = () => { ) } + + diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts index 236d31b9ff..a23ba980fc 100644 --- a/src/stores/versionCompatibilityStore.ts +++ b/src/stores/versionCompatibilityStore.ts @@ -13,7 +13,9 @@ export const useVersionCompatibilityStore = defineStore( const settingStore = useSettingStore() const isDismissed = ref(false) - const dismissedVersion = ref(null) + const dismissedVersion = ref( + settingStore.get('Comfy.VersionMismatch.DismissedVersion') ?? null + ) const frontendVersion = computed(() => config.app_version) const backendVersion = computed( diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index 73dea2212a..2de7eeed78 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -217,9 +217,6 @@ onMounted(() => { } catch (e) { console.error('Failed to init ComfyUI frontend', e) } - - // Initialize version compatibility checking (fire-and-forget) - void versionCompatibilityStore.initialize() }) onBeforeUnmount(() => { @@ -257,6 +254,7 @@ const onGraphReady = () => { // Explicitly initialize nodeSearchService to avoid indexing delay when // node search is triggered useNodeDefStore().nodeSearchService.searchNode('') + void versionCompatibilityStore.initialize() }, { timeout: 1000 } ) From 6a36557c698e35b42aaa3c38735c7e15e8858d28 Mon Sep 17 00:00:00 2001 From: SHIVANSH GUPTA <121501003+shivansh-gupta4@users.noreply.github.com> Date: Mon, 21 Jul 2025 01:04:02 +0530 Subject: [PATCH 05/12] Update VersionMismatchWarning.vue --- src/components/dialog/content/VersionMismatchWarning.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/dialog/content/VersionMismatchWarning.vue b/src/components/dialog/content/VersionMismatchWarning.vue index 4efcaf4df3..e834b23c8b 100644 --- a/src/components/dialog/content/VersionMismatchWarning.vue +++ b/src/components/dialog/content/VersionMismatchWarning.vue @@ -9,7 +9,6 @@ text: { class: 'flex-1' }, icon: { class: 'flex items-start mt-1' } }" - @close="handleDismiss" >
From 4c615791958afddb6d9258b349fc3e81406308a2 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 26 Jul 2025 12:24:37 -0700 Subject: [PATCH 06/12] [fix] Improve version mismatch warning system with automatic dismissal and code cleanup - Fix dismissal persistence: warnings now automatically dismiss for 7 days when shown - Eliminate code duplication by consolidating key generation into computed properties - Replace Vue component with composable for better reusability and testability - Add comprehensive test coverage for both composable and store - Ensure clean separation of concerns between display logic and state management --- .../dialog/content/VersionMismatchWarning.vue | 85 ------ .../useFrontendVersionMismatchWarning.ts | 81 ++++++ src/locales/en/main.json | 6 + src/stores/versionCompatibilityStore.ts | 72 ++--- src/views/GraphView.vue | 10 +- .../useFrontendVersionMismatchWarning.test.ts | 257 ++++++++++++++++++ .../store/versionCompatibilityStore.test.ts | 84 ++++-- 7 files changed, 451 insertions(+), 144 deletions(-) delete mode 100644 src/components/dialog/content/VersionMismatchWarning.vue create mode 100644 src/composables/useFrontendVersionMismatchWarning.ts create mode 100644 tests-ui/tests/composables/useFrontendVersionMismatchWarning.test.ts diff --git a/src/components/dialog/content/VersionMismatchWarning.vue b/src/components/dialog/content/VersionMismatchWarning.vue deleted file mode 100644 index e834b23c8b..0000000000 --- a/src/components/dialog/content/VersionMismatchWarning.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - - - diff --git a/src/composables/useFrontendVersionMismatchWarning.ts b/src/composables/useFrontendVersionMismatchWarning.ts new file mode 100644 index 0000000000..72d79c5ce0 --- /dev/null +++ b/src/composables/useFrontendVersionMismatchWarning.ts @@ -0,0 +1,81 @@ +import { whenever } from '@vueuse/core' +import { computed, onMounted } from 'vue' +import { useI18n } from 'vue-i18n' + +import { useToastStore } from '@/stores/toastStore' +import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore' + +export interface UseFrontendVersionMismatchWarningOptions { + immediate?: boolean +} + +export function useFrontendVersionMismatchWarning( + options: UseFrontendVersionMismatchWarningOptions = {} +) { + const { immediate = false } = options + const { t } = useI18n() + const toastStore = useToastStore() + const versionCompatibilityStore = useVersionCompatibilityStore() + + // Track if we've already shown the warning + let hasShownWarning = false + + const showWarning = () => { + // Prevent showing the warning multiple times + if (hasShownWarning) return + + const message = versionCompatibilityStore.warningMessage + if (!message) return + + const detailMessage = + message.type === 'outdated' + ? t('g.frontendOutdated', { + frontendVersion: message.frontendVersion, + requiredVersion: message.requiredVersion + }) + : t('g.frontendNewer', { + frontendVersion: message.frontendVersion, + backendVersion: message.backendVersion + }) + + const fullMessage = t('g.versionMismatchWarningMessage', { + warning: t('g.versionMismatchWarning'), + detail: detailMessage + }) + + toastStore.addAlert(fullMessage) + hasShownWarning = true + + // Automatically dismiss the warning so it won't show again for 7 days + versionCompatibilityStore.dismissWarning() + } + + onMounted(() => { + // Only set up the watcher if immediate is true + if (immediate) { + whenever( + () => versionCompatibilityStore.shouldShowWarning, + (shouldShow) => { + if (shouldShow) { + showWarning() + } + }, + { + immediate: true, + once: true + } + ) + } + }) + + return { + showWarning, + shouldShowWarning: computed( + () => versionCompatibilityStore.shouldShowWarning + ), + dismissWarning: versionCompatibilityStore.dismissWarning, + hasVersionMismatch: computed( + () => versionCompatibilityStore.hasVersionMismatch + ) + } +} diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 46c6a2ded1..5fa4aa4226 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -98,6 +98,12 @@ "nodes": "Nodes", "community": "Community", "all": "All", + "versionMismatchWarning": "Version Compatibility Warning", + "versionMismatchWarningMessage": "{warning}: {detail} Visit https://docs.comfy.org/installation/update_comfyui#common-update-issues for update instructions.", + "frontendOutdated": "Frontend version {frontendVersion} is outdated. Backend requires {requiredVersion} or higher.", + "frontendNewer": "Frontend version {frontendVersion} may not be compatible with backend version {backendVersion}.", + "updateFrontend": "Update Frontend", + "dismiss": "Dismiss", "update": "Update", "updated": "Updated", "resultsCount": "Found {count} Results", diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts index a23ba980fc..769713f863 100644 --- a/src/stores/versionCompatibilityStore.ts +++ b/src/stores/versionCompatibilityStore.ts @@ -1,21 +1,17 @@ import { defineStore } from 'pinia' -import { computed, ref } from 'vue' +import { computed } from 'vue' import config from '@/config' -import { useSettingStore } from '@/stores/settingStore' import { useSystemStatsStore } from '@/stores/systemStatsStore' import { compareVersions, isSemVer } from '@/utils/formatUtil' +const DISMISSAL_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days +const DISMISSAL_KEY_PREFIX = 'comfy.versionMismatch.dismissed.' + export const useVersionCompatibilityStore = defineStore( 'versionCompatibility', () => { const systemStatsStore = useSystemStatsStore() - const settingStore = useSettingStore() - - const isDismissed = ref(false) - const dismissedVersion = ref( - settingStore.get('Comfy.VersionMismatch.DismissedVersion') ?? null - ) const frontendVersion = computed(() => config.app_version) const backendVersion = computed( @@ -61,16 +57,38 @@ export const useVersionCompatibilityStore = defineStore( return isFrontendOutdated.value || isFrontendNewer.value }) - const currentVersionKey = computed( - () => - `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` - ) + const versionKey = computed(() => { + if ( + !frontendVersion.value || + !backendVersion.value || + !requiredFrontendVersion.value + ) { + return null + } + return `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` + }) + + const dismissalKey = computed(() => { + if (!versionKey.value) return null + return DISMISSAL_KEY_PREFIX + versionKey.value + }) + + const isDismissed = computed(() => { + if (!dismissalKey.value) return false + + const dismissedUntil = localStorage.getItem(dismissalKey.value) + + if (!dismissedUntil) return false + + const dismissedUntilTime = parseInt(dismissedUntil, 10) + if (isNaN(dismissedUntilTime)) return false + + // Check if dismissal has expired + return Date.now() < dismissedUntilTime + }) const shouldShowWarning = computed(() => { - if (!hasVersionMismatch.value || isDismissed.value) { - return false - } - return dismissedVersion.value !== currentVersionKey.value + return hasVersionMismatch.value && !isDismissed.value }) const warningMessage = computed(() => { @@ -96,29 +114,15 @@ export const useVersionCompatibilityStore = defineStore( } } - async function dismissWarning() { - isDismissed.value = true - dismissedVersion.value = currentVersionKey.value - - await settingStore.set( - 'Comfy.VersionMismatch.DismissedVersion', - currentVersionKey.value - ) - } + function dismissWarning() { + if (!dismissalKey.value) return - function restoreDismissalState() { - const dismissed = settingStore.get( - 'Comfy.VersionMismatch.DismissedVersion' - ) - if (dismissed) { - dismissedVersion.value = dismissed - isDismissed.value = dismissed === currentVersionKey.value - } + const dismissUntil = Date.now() + DISMISSAL_DURATION_MS + localStorage.setItem(dismissalKey.value, dismissUntil.toString()) } async function initialize() { await checkVersionCompatibility() - restoreDismissalState() } return { diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index 2de7eeed78..5a59e0aee3 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -9,7 +9,6 @@
-
@@ -29,7 +28,6 @@ import { useI18n } from 'vue-i18n' import MenuHamburger from '@/components/MenuHamburger.vue' import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue' -import VersionMismatchWarning from '@/components/dialog/content/VersionMismatchWarning.vue' import GraphCanvas from '@/components/graph/GraphCanvas.vue' import GlobalToast from '@/components/toast/GlobalToast.vue' import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue' @@ -37,6 +35,7 @@ import TopMenubar from '@/components/topbar/TopMenubar.vue' import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle' import { useCoreCommands } from '@/composables/useCoreCommands' import { useErrorHandling } from '@/composables/useErrorHandling' +import { useFrontendVersionMismatchWarning } from '@/composables/useFrontendVersionMismatchWarning' import { useProgressFavicon } from '@/composables/useProgressFavicon' import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig' import { i18n } from '@/i18n' @@ -229,6 +228,11 @@ onBeforeUnmount(() => { useEventListener(window, 'keydown', useKeybindingService().keybindHandler) const { wrapWithErrorHandling, wrapWithErrorHandlingAsync } = useErrorHandling() + +// Initialize version mismatch warning in setup context +// It will be triggered automatically when the store is ready +useFrontendVersionMismatchWarning({ immediate: true }) + const onGraphReady = () => { requestIdleCallback( () => { @@ -254,6 +258,8 @@ const onGraphReady = () => { // Explicitly initialize nodeSearchService to avoid indexing delay when // node search is triggered useNodeDefStore().nodeSearchService.searchNode('') + + // Initialize version compatibility store void versionCompatibilityStore.initialize() }, { timeout: 1000 } diff --git a/tests-ui/tests/composables/useFrontendVersionMismatchWarning.test.ts b/tests-ui/tests/composables/useFrontendVersionMismatchWarning.test.ts new file mode 100644 index 0000000000..b4ed7c2634 --- /dev/null +++ b/tests-ui/tests/composables/useFrontendVersionMismatchWarning.test.ts @@ -0,0 +1,257 @@ +import { createPinia, setActivePinia } from 'pinia' +import { vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { nextTick } from 'vue' + +import { useFrontendVersionMismatchWarning } from '@/composables/useFrontendVersionMismatchWarning' +import { useToastStore } from '@/stores/toastStore' +import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore' + +// Mock globals +//@ts-expect-error Define global for the test +global.__COMFYUI_FRONTEND_VERSION__ = '1.0.0' + +// Mock config first - this needs to be before any imports +vi.mock('@/config', () => ({ + default: { + app_title: 'ComfyUI', + app_version: '1.0.0' + } +})) + +// Mock app +vi.mock('@/scripts/app', () => ({ + app: { + ui: { + settings: { + dispatchChange: vi.fn() + } + } + } +})) + +// Mock api +vi.mock('@/scripts/api', () => ({ + api: { + getSettings: vi.fn(() => Promise.resolve({})), + storeSetting: vi.fn(() => Promise.resolve(undefined)) + } +})) + +// Mock vue-i18n +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key: string, params?: any) => { + if (key === 'g.versionMismatchWarning') + return 'Version Compatibility Warning' + if (key === 'g.versionMismatchWarningMessage' && params) { + return `${params.warning}: ${params.detail} Visit https://docs.comfy.org/installation/update_comfyui#common-update-issues for update instructions.` + } + if (key === 'g.frontendOutdated' && params) { + return `Frontend version ${params.frontendVersion} is outdated. Backend requires ${params.requiredVersion} or higher.` + } + if (key === 'g.frontendNewer' && params) { + return `Frontend version ${params.frontendVersion} may not be compatible with backend version ${params.backendVersion}.` + } + return key + } + }), + createI18n: vi.fn(() => ({ + global: { + locale: { value: 'en' }, + t: vi.fn() + } + })) +})) + +// Mock lifecycle hooks to track their calls +const mockOnMounted = vi.fn() +vi.mock('vue', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + onMounted: (fn: () => void) => { + mockOnMounted() + fn() + } + } +}) + +describe('useFrontendVersionMismatchWarning', () => { + beforeEach(() => { + vi.clearAllMocks() + setActivePinia(createPinia()) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should not show warning when there is no version mismatch', () => { + const toastStore = useToastStore() + const versionStore = useVersionCompatibilityStore() + const addAlertSpy = vi.spyOn(toastStore, 'addAlert') + + // Mock no version mismatch + vi.spyOn(versionStore, 'shouldShowWarning', 'get').mockReturnValue(false) + + useFrontendVersionMismatchWarning() + + expect(addAlertSpy).not.toHaveBeenCalled() + }) + + it('should show warning immediately when immediate option is true and there is a mismatch', async () => { + const toastStore = useToastStore() + const versionStore = useVersionCompatibilityStore() + const addAlertSpy = vi.spyOn(toastStore, 'addAlert') + const dismissWarningSpy = vi.spyOn(versionStore, 'dismissWarning') + + // Mock version mismatch + vi.spyOn(versionStore, 'shouldShowWarning', 'get').mockReturnValue(true) + vi.spyOn(versionStore, 'warningMessage', 'get').mockReturnValue({ + type: 'outdated', + frontendVersion: '1.0.0', + requiredVersion: '2.0.0' + }) + + useFrontendVersionMismatchWarning({ immediate: true }) + + // For immediate: true, the watcher should fire immediately in onMounted + await nextTick() + + expect(addAlertSpy).toHaveBeenCalledWith( + expect.stringContaining('Version Compatibility Warning') + ) + expect(addAlertSpy).toHaveBeenCalledWith( + expect.stringContaining('Frontend version 1.0.0 is outdated') + ) + // Should automatically dismiss the warning + expect(dismissWarningSpy).toHaveBeenCalled() + }) + + it('should not show warning immediately when immediate option is false', async () => { + const toastStore = useToastStore() + const versionStore = useVersionCompatibilityStore() + const addAlertSpy = vi.spyOn(toastStore, 'addAlert') + + // Mock version mismatch + vi.spyOn(versionStore, 'shouldShowWarning', 'get').mockReturnValue(true) + vi.spyOn(versionStore, 'warningMessage', 'get').mockReturnValue({ + type: 'outdated', + frontendVersion: '1.0.0', + requiredVersion: '2.0.0' + }) + + const result = useFrontendVersionMismatchWarning({ immediate: false }) + await nextTick() + + // Should not show automatically + expect(addAlertSpy).not.toHaveBeenCalled() + + // But should show when called manually + result.showWarning() + expect(addAlertSpy).toHaveBeenCalledOnce() + }) + + it('should show warning for newer frontend version', async () => { + const toastStore = useToastStore() + const versionStore = useVersionCompatibilityStore() + const addAlertSpy = vi.spyOn(toastStore, 'addAlert') + const dismissWarningSpy = vi.spyOn(versionStore, 'dismissWarning') + + // Mock version mismatch with newer frontend + vi.spyOn(versionStore, 'shouldShowWarning', 'get').mockReturnValue(true) + vi.spyOn(versionStore, 'warningMessage', 'get').mockReturnValue({ + type: 'newer', + frontendVersion: '2.0.0', + backendVersion: '1.0.0' + }) + + useFrontendVersionMismatchWarning({ immediate: true }) + await nextTick() + + expect(addAlertSpy).toHaveBeenCalledWith( + expect.stringContaining('Frontend version 2.0.0 may not be compatible') + ) + expect(dismissWarningSpy).toHaveBeenCalled() + }) + + it('should call showWarning method manually', () => { + const toastStore = useToastStore() + const versionStore = useVersionCompatibilityStore() + const addAlertSpy = vi.spyOn(toastStore, 'addAlert') + const dismissWarningSpy = vi.spyOn(versionStore, 'dismissWarning') + + vi.spyOn(versionStore, 'warningMessage', 'get').mockReturnValue({ + type: 'outdated', + frontendVersion: '1.0.0', + requiredVersion: '2.0.0' + }) + + const { showWarning } = useFrontendVersionMismatchWarning() + showWarning() + + expect(addAlertSpy).toHaveBeenCalledOnce() + expect(dismissWarningSpy).toHaveBeenCalled() + }) + + it('should expose store methods and computed values', () => { + const versionStore = useVersionCompatibilityStore() + + const mockDismissWarning = vi.fn() + vi.spyOn(versionStore, 'dismissWarning').mockImplementation( + mockDismissWarning + ) + vi.spyOn(versionStore, 'shouldShowWarning', 'get').mockReturnValue(true) + vi.spyOn(versionStore, 'hasVersionMismatch', 'get').mockReturnValue(true) + + const result = useFrontendVersionMismatchWarning() + + expect(result.shouldShowWarning.value).toBe(true) + expect(result.hasVersionMismatch.value).toBe(true) + + void result.dismissWarning() + expect(mockDismissWarning).toHaveBeenCalled() + }) + + it('should register onMounted hook', () => { + useFrontendVersionMismatchWarning() + + expect(mockOnMounted).toHaveBeenCalledOnce() + }) + + it('should not show warning when warningMessage is null', () => { + const toastStore = useToastStore() + const versionStore = useVersionCompatibilityStore() + const addAlertSpy = vi.spyOn(toastStore, 'addAlert') + + vi.spyOn(versionStore, 'warningMessage', 'get').mockReturnValue(null) + + const { showWarning } = useFrontendVersionMismatchWarning() + showWarning() + + expect(addAlertSpy).not.toHaveBeenCalled() + }) + + it('should only show warning once even if called multiple times', () => { + const toastStore = useToastStore() + const versionStore = useVersionCompatibilityStore() + const addAlertSpy = vi.spyOn(toastStore, 'addAlert') + + vi.spyOn(versionStore, 'warningMessage', 'get').mockReturnValue({ + type: 'outdated', + frontendVersion: '1.0.0', + requiredVersion: '2.0.0' + }) + + const { showWarning } = useFrontendVersionMismatchWarning() + + // Call showWarning multiple times + showWarning() + showWarning() + showWarning() + + // Should only have been called once + expect(addAlertSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts index 4f72f62305..219de865b1 100644 --- a/tests-ui/tests/store/versionCompatibilityStore.test.ts +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -1,7 +1,6 @@ import { createPinia, setActivePinia } from 'pinia' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { useSettingStore } from '@/stores/settingStore' import { useSystemStatsStore } from '@/stores/systemStatsStore' import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore' @@ -12,32 +11,46 @@ vi.mock('@/config', () => ({ })) vi.mock('@/stores/systemStatsStore') -vi.mock('@/stores/settingStore') + +// Mock localStorage +const localStorageMock = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn() +} +Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true +}) describe('useVersionCompatibilityStore', () => { let store: ReturnType let mockSystemStatsStore: any - let mockSettingStore: any beforeEach(() => { setActivePinia(createPinia()) + // Clear localStorage mock + localStorageMock.getItem.mockReset() + localStorageMock.setItem.mockReset() + localStorageMock.removeItem.mockReset() + localStorageMock.clear.mockReset() + mockSystemStatsStore = { systemStats: null, fetchSystemStats: vi.fn() } - mockSettingStore = { - get: vi.fn(), - set: vi.fn() - } - vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore) - vi.mocked(useSettingStore).mockReturnValue(mockSettingStore) store = useVersionCompatibilityStore() }) + afterEach(() => { + vi.clearAllMocks() + }) + describe('version compatibility detection', () => { it('should detect frontend is outdated when required version is higher', async () => { mockSystemStatsStore.systemStats = { @@ -116,11 +129,8 @@ describe('useVersionCompatibilityStore', () => { }) describe('warning display logic', () => { - beforeEach(() => { - mockSettingStore.get.mockReturnValue('') - }) - it('should show warning when there is a version mismatch and not dismissed', async () => { + localStorageMock.getItem.mockReturnValue(null) // Not dismissed mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.25.0', @@ -134,6 +144,9 @@ describe('useVersionCompatibilityStore', () => { }) it('should not show warning when dismissed', async () => { + const futureTime = Date.now() + 1000000 + localStorageMock.getItem.mockReturnValue(String(futureTime)) + mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.25.0', @@ -142,7 +155,6 @@ describe('useVersionCompatibilityStore', () => { } await store.checkVersionCompatibility() - void store.dismissWarning() expect(store.shouldShowWarning).toBe(false) }) @@ -211,7 +223,10 @@ describe('useVersionCompatibilityStore', () => { }) describe('dismissal persistence', () => { - it('should save dismissal to settings', async () => { + it('should save dismissal to localStorage with expiration', async () => { + const mockNow = 1000000 + vi.spyOn(Date, 'now').mockReturnValue(mockNow) + mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.25.0', @@ -220,16 +235,18 @@ describe('useVersionCompatibilityStore', () => { } await store.checkVersionCompatibility() - await store.dismissWarning() + store.dismissWarning() - expect(mockSettingStore.set).toHaveBeenCalledWith( - 'Comfy.VersionMismatch.DismissedVersion', - '1.24.0-1.25.0-1.25.0' + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'comfy.versionMismatch.dismissed.1.24.0-1.25.0-1.25.0', + String(mockNow + 7 * 24 * 60 * 60 * 1000) // 7 days later ) }) - it('should restore dismissal state from settings', async () => { - mockSettingStore.get.mockReturnValue('1.24.0-1.25.0-1.25.0') + it('should check dismissal state from localStorage', async () => { + const futureTime = Date.now() + 1000000 // Still valid + localStorageMock.getItem.mockReturnValue(String(futureTime)) + mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.25.0', @@ -242,8 +259,29 @@ describe('useVersionCompatibilityStore', () => { expect(store.shouldShowWarning).toBe(false) }) + it('should show warning if dismissal has expired', async () => { + const pastTime = Date.now() - 1000 // Expired + localStorageMock.getItem.mockReturnValue(String(pastTime)) + + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.25.0', + required_frontend_version: '1.25.0' + } + } + + await store.initialize() + + expect(store.shouldShowWarning).toBe(true) + }) + it('should show warning for different version combinations even if previous was dismissed', async () => { - mockSettingStore.get.mockReturnValue('1.24.0-1.25.0-1.25.0') + const futureTime = Date.now() + 1000000 + // Dismissed for different version combination + localStorageMock.getItem + .mockReturnValueOnce(null) // Current version key not found + .mockReturnValueOnce(String(futureTime)) // Different version was dismissed + mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.26.0', From dcfe11e491834b455d62c93db5496a50dd57c00a Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 26 Jul 2025 12:56:31 -0700 Subject: [PATCH 07/12] [improve] Implement feedback improvements for version mismatch warning system - Add comprehensive JSDoc documentation to composable - Replace localStorage with vueuse useStorage for better reactivity and client/server compatibility - Simplify whenever callback by removing unnecessary boolean parameter check - Update all tests to work with new useStorage-based storage format - Maintain full functionality while improving code quality and maintainability --- .../useFrontendVersionMismatchWarning.ts | 27 ++++++++++-- src/stores/versionCompatibilityStore.ts | 41 ++++++++++++------- .../store/versionCompatibilityStore.test.ts | 36 +++++++++++----- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/src/composables/useFrontendVersionMismatchWarning.ts b/src/composables/useFrontendVersionMismatchWarning.ts index 72d79c5ce0..676cd58a56 100644 --- a/src/composables/useFrontendVersionMismatchWarning.ts +++ b/src/composables/useFrontendVersionMismatchWarning.ts @@ -9,6 +9,27 @@ export interface UseFrontendVersionMismatchWarningOptions { immediate?: boolean } +/** + * Composable for handling frontend version mismatch warnings. + * + * Displays toast notifications when the frontend version is incompatible with the backend, + * either because the frontend is outdated or newer than the backend expects. + * Automatically dismisses warnings when shown and persists dismissal state for 7 days. + * + * @param options - Configuration options + * @param options.immediate - If true, automatically shows warning when version mismatch is detected + * @returns Object with methods and computed properties for managing version warnings + * + * @example + * ```ts + * // Show warning immediately when mismatch detected + * const { showWarning, shouldShowWarning } = useFrontendVersionMismatchWarning({ immediate: true }) + * + * // Manual control + * const { showWarning } = useFrontendVersionMismatchWarning() + * showWarning() // Call when needed + * ``` + */ export function useFrontendVersionMismatchWarning( options: UseFrontendVersionMismatchWarningOptions = {} ) { @@ -55,10 +76,8 @@ export function useFrontendVersionMismatchWarning( if (immediate) { whenever( () => versionCompatibilityStore.shouldShowWarning, - (shouldShow) => { - if (shouldShow) { - showWarning() - } + () => { + showWarning() }, { immediate: true, diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts index 769713f863..fe74e5891d 100644 --- a/src/stores/versionCompatibilityStore.ts +++ b/src/stores/versionCompatibilityStore.ts @@ -1,3 +1,4 @@ +import { useStorage } from '@vueuse/core' import { defineStore } from 'pinia' import { computed } from 'vue' @@ -6,7 +7,6 @@ import { useSystemStatsStore } from '@/stores/systemStatsStore' import { compareVersions, isSemVer } from '@/utils/formatUtil' const DISMISSAL_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days -const DISMISSAL_KEY_PREFIX = 'comfy.versionMismatch.dismissed.' export const useVersionCompatibilityStore = defineStore( 'versionCompatibility', @@ -68,23 +68,33 @@ export const useVersionCompatibilityStore = defineStore( return `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}` }) - const dismissalKey = computed(() => { - if (!versionKey.value) return null - return DISMISSAL_KEY_PREFIX + versionKey.value - }) + // Use reactive storage for dismissals - creates a reactive ref that syncs with localStorage + const dismissalStorage = useStorage( + 'comfy.versionMismatch.dismissed', + {} as Record, + localStorage, + { + serializer: { + read: (value: string) => { + try { + return JSON.parse(value) + } catch { + return {} + } + }, + write: (value: Record) => JSON.stringify(value) + } + } + ) const isDismissed = computed(() => { - if (!dismissalKey.value) return false - - const dismissedUntil = localStorage.getItem(dismissalKey.value) + if (!versionKey.value) return false + const dismissedUntil = dismissalStorage.value[versionKey.value] if (!dismissedUntil) return false - const dismissedUntilTime = parseInt(dismissedUntil, 10) - if (isNaN(dismissedUntilTime)) return false - // Check if dismissal has expired - return Date.now() < dismissedUntilTime + return Date.now() < dismissedUntil }) const shouldShowWarning = computed(() => { @@ -115,10 +125,13 @@ export const useVersionCompatibilityStore = defineStore( } function dismissWarning() { - if (!dismissalKey.value) return + if (!versionKey.value) return const dismissUntil = Date.now() + DISMISSAL_DURATION_MS - localStorage.setItem(dismissalKey.value, dismissUntil.toString()) + dismissalStorage.value = { + ...dismissalStorage.value, + [versionKey.value]: dismissUntil + } } async function initialize() { diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts index 219de865b1..6ff59b660d 100644 --- a/tests-ui/tests/store/versionCompatibilityStore.test.ts +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -130,7 +130,7 @@ describe('useVersionCompatibilityStore', () => { describe('warning display logic', () => { it('should show warning when there is a version mismatch and not dismissed', async () => { - localStorageMock.getItem.mockReturnValue(null) // Not dismissed + localStorageMock.getItem.mockReturnValue('{}') // Empty dismissal storage mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.25.0', @@ -145,7 +145,11 @@ describe('useVersionCompatibilityStore', () => { it('should not show warning when dismissed', async () => { const futureTime = Date.now() + 1000000 - localStorageMock.getItem.mockReturnValue(String(futureTime)) + localStorageMock.getItem.mockReturnValue( + JSON.stringify({ + '1.24.0-1.25.0-1.25.0': futureTime + }) + ) mockSystemStatsStore.systemStats = { system: { @@ -238,14 +242,20 @@ describe('useVersionCompatibilityStore', () => { store.dismissWarning() expect(localStorageMock.setItem).toHaveBeenCalledWith( - 'comfy.versionMismatch.dismissed.1.24.0-1.25.0-1.25.0', - String(mockNow + 7 * 24 * 60 * 60 * 1000) // 7 days later + 'comfy.versionMismatch.dismissed', + JSON.stringify({ + '1.24.0-1.25.0-1.25.0': mockNow + 7 * 24 * 60 * 60 * 1000 + }) ) }) it('should check dismissal state from localStorage', async () => { const futureTime = Date.now() + 1000000 // Still valid - localStorageMock.getItem.mockReturnValue(String(futureTime)) + localStorageMock.getItem.mockReturnValue( + JSON.stringify({ + '1.24.0-1.25.0-1.25.0': futureTime + }) + ) mockSystemStatsStore.systemStats = { system: { @@ -261,7 +271,11 @@ describe('useVersionCompatibilityStore', () => { it('should show warning if dismissal has expired', async () => { const pastTime = Date.now() - 1000 // Expired - localStorageMock.getItem.mockReturnValue(String(pastTime)) + localStorageMock.getItem.mockReturnValue( + JSON.stringify({ + '1.24.0-1.25.0-1.25.0': pastTime + }) + ) mockSystemStatsStore.systemStats = { system: { @@ -277,10 +291,12 @@ describe('useVersionCompatibilityStore', () => { it('should show warning for different version combinations even if previous was dismissed', async () => { const futureTime = Date.now() + 1000000 - // Dismissed for different version combination - localStorageMock.getItem - .mockReturnValueOnce(null) // Current version key not found - .mockReturnValueOnce(String(futureTime)) // Different version was dismissed + // Dismissed for different version combination (1.25.0) but current is 1.26.0 + localStorageMock.getItem.mockReturnValue( + JSON.stringify({ + '1.24.0-1.25.0-1.25.0': futureTime // Different version was dismissed + }) + ) mockSystemStatsStore.systemStats = { system: { From 02b85e191ca7a35a1272d98091557a44d43136e7 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 26 Jul 2025 13:14:10 -0700 Subject: [PATCH 08/12] [refactor] Improve localStorage organization and fix test mocking - Consolidate all version mismatch dismissals into single 'comfy.versionMismatch.dismissals' key - Prevents localStorage clutter with multiple individual keys - Fix test suite to properly mock useStorage from VueUse instead of direct localStorage - Use reactive storage mocking for better test reliability and accuracy - All tests now pass with proper reactive behavior verification --- src/stores/versionCompatibilityStore.ts | 3 +- .../store/versionCompatibilityStore.test.ts | 74 ++++++++----------- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts index fe74e5891d..3fd7daa306 100644 --- a/src/stores/versionCompatibilityStore.ts +++ b/src/stores/versionCompatibilityStore.ts @@ -69,8 +69,9 @@ export const useVersionCompatibilityStore = defineStore( }) // Use reactive storage for dismissals - creates a reactive ref that syncs with localStorage + // All version mismatch dismissals are stored in a single object for clean localStorage organization const dismissalStorage = useStorage( - 'comfy.versionMismatch.dismissed', + 'comfy.versionMismatch.dismissals', {} as Record, localStorage, { diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts index 6ff59b660d..c89027fd04 100644 --- a/tests-ui/tests/store/versionCompatibilityStore.test.ts +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -1,5 +1,6 @@ import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ref } from 'vue' import { useSystemStatsStore } from '@/stores/systemStatsStore' import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore' @@ -12,17 +13,11 @@ vi.mock('@/config', () => ({ vi.mock('@/stores/systemStatsStore') -// Mock localStorage -const localStorageMock = { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn() -} -Object.defineProperty(window, 'localStorage', { - value: localStorageMock, - writable: true -}) +// Mock useStorage from VueUse +const mockDismissalStorage = ref({} as Record) +vi.mock('@vueuse/core', () => ({ + useStorage: vi.fn(() => mockDismissalStorage) +})) describe('useVersionCompatibilityStore', () => { let store: ReturnType @@ -31,11 +26,8 @@ describe('useVersionCompatibilityStore', () => { beforeEach(() => { setActivePinia(createPinia()) - // Clear localStorage mock - localStorageMock.getItem.mockReset() - localStorageMock.setItem.mockReset() - localStorageMock.removeItem.mockReset() - localStorageMock.clear.mockReset() + // Clear the mock dismissal storage + mockDismissalStorage.value = {} mockSystemStatsStore = { systemStats: null, @@ -130,7 +122,8 @@ describe('useVersionCompatibilityStore', () => { describe('warning display logic', () => { it('should show warning when there is a version mismatch and not dismissed', async () => { - localStorageMock.getItem.mockReturnValue('{}') // Empty dismissal storage + // No dismissals in storage + mockDismissalStorage.value = {} mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.25.0', @@ -145,11 +138,10 @@ describe('useVersionCompatibilityStore', () => { it('should not show warning when dismissed', async () => { const futureTime = Date.now() + 1000000 - localStorageMock.getItem.mockReturnValue( - JSON.stringify({ - '1.24.0-1.25.0-1.25.0': futureTime - }) - ) + // Set dismissal in reactive storage + mockDismissalStorage.value = { + '1.24.0-1.25.0-1.25.0': futureTime + } mockSystemStatsStore.systemStats = { system: { @@ -227,7 +219,7 @@ describe('useVersionCompatibilityStore', () => { }) describe('dismissal persistence', () => { - it('should save dismissal to localStorage with expiration', async () => { + it('should save dismissal to reactive storage with expiration', async () => { const mockNow = 1000000 vi.spyOn(Date, 'now').mockReturnValue(mockNow) @@ -241,21 +233,17 @@ describe('useVersionCompatibilityStore', () => { await store.checkVersionCompatibility() store.dismissWarning() - expect(localStorageMock.setItem).toHaveBeenCalledWith( - 'comfy.versionMismatch.dismissed', - JSON.stringify({ - '1.24.0-1.25.0-1.25.0': mockNow + 7 * 24 * 60 * 60 * 1000 - }) - ) + // Check that the dismissal was added to reactive storage + expect(mockDismissalStorage.value).toEqual({ + '1.24.0-1.25.0-1.25.0': mockNow + 7 * 24 * 60 * 60 * 1000 + }) }) - it('should check dismissal state from localStorage', async () => { + it('should check dismissal state from reactive storage', async () => { const futureTime = Date.now() + 1000000 // Still valid - localStorageMock.getItem.mockReturnValue( - JSON.stringify({ - '1.24.0-1.25.0-1.25.0': futureTime - }) - ) + mockDismissalStorage.value = { + '1.24.0-1.25.0-1.25.0': futureTime + } mockSystemStatsStore.systemStats = { system: { @@ -271,11 +259,9 @@ describe('useVersionCompatibilityStore', () => { it('should show warning if dismissal has expired', async () => { const pastTime = Date.now() - 1000 // Expired - localStorageMock.getItem.mockReturnValue( - JSON.stringify({ - '1.24.0-1.25.0-1.25.0': pastTime - }) - ) + mockDismissalStorage.value = { + '1.24.0-1.25.0-1.25.0': pastTime + } mockSystemStatsStore.systemStats = { system: { @@ -292,11 +278,9 @@ describe('useVersionCompatibilityStore', () => { it('should show warning for different version combinations even if previous was dismissed', async () => { const futureTime = Date.now() + 1000000 // Dismissed for different version combination (1.25.0) but current is 1.26.0 - localStorageMock.getItem.mockReturnValue( - JSON.stringify({ - '1.24.0-1.25.0-1.25.0': futureTime // Different version was dismissed - }) - ) + mockDismissalStorage.value = { + '1.24.0-1.25.0-1.25.0': futureTime // Different version was dismissed + } mockSystemStatsStore.systemStats = { system: { From 1ad0aa7dccaebb33add561857eef9f43adf6507a Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 26 Jul 2025 19:01:12 -0700 Subject: [PATCH 09/12] [bugfix] Fix version mismatch warning logic to use semver library - Replace custom version comparison with robust semver library - Fix bug where warnings showed when frontend meets/exceeds required version - Only warn when frontend is significantly ahead (>2 minor versions or major version mismatch) - Don't warn when there's no required version but frontend is newer than backend - Add comprehensive test coverage for all warning scenarios Co-Authored-By: Claude --- package-lock.json | 198 ++++-------------- package.json | 2 + src/stores/versionCompatibilityStore.ts | 66 ++++-- src/views/GraphView.vue | 23 +- .../store/versionCompatibilityStore.test.ts | 84 +++++++- 5 files changed, 190 insertions(+), 183 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa5cee570b..5ec43f6632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "pinia": "^2.1.7", "primeicons": "^7.0.0", "primevue": "^4.2.5", + "semver": "^7.7.2", "three": "^0.170.0", "tiptap-markdown": "^0.8.10", "vue": "^3.5.13", @@ -62,6 +63,7 @@ "@types/fs-extra": "^11.0.4", "@types/lodash": "^4.17.6", "@types/node": "^20.14.8", + "@types/semver": "^7.7.0", "@types/three": "^0.169.0", "@vitejs/plugin-vue": "^5.1.4", "@vue/test-utils": "^2.4.6", @@ -557,6 +559,15 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", @@ -601,6 +612,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", @@ -622,6 +642,15 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", @@ -2422,18 +2451,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@intlify/eslint-plugin-vue-i18n/node_modules/synckit": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.3.tgz", @@ -4523,6 +4540,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true + }, "node_modules/@types/stats.js": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", @@ -4754,19 +4777,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0.tgz", @@ -6537,19 +6547,6 @@ "dev": true, "license": "MIT" }, - "node_modules/conf/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/confbox": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", @@ -7449,19 +7446,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/editorconfig/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7741,18 +7725,6 @@ "eslint": ">=6.0.0" } }, - "node_modules/eslint-compat-utils/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-config-prettier": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz", @@ -7852,19 +7824,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-vue/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-plugin-vue/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -10284,18 +10243,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/jsonc-eslint-parser/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jsondiffpatch": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", @@ -10726,19 +10673,6 @@ "node": ">=14" } }, - "node_modules/langsmith/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/latest-version": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", @@ -12688,19 +12622,6 @@ "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", "dev": true }, - "node_modules/package-json/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/package-manager-detector": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.0.tgz", @@ -14343,12 +14264,14 @@ "dev": true }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -16207,19 +16130,6 @@ "url": "https://github.com/yeoman/update-notifier?sponsor=1" } }, - "node_modules/update-notifier/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -17039,19 +16949,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/vue-eslint-parser/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vue-i18n": { "version": "9.14.3", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.3.tgz", @@ -17104,19 +17001,6 @@ "typescript": ">=5.0.0" } }, - "node_modules/vue-tsc/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vuefire": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/vuefire/-/vuefire-3.2.1.tgz", diff --git a/package.json b/package.json index 2dfd9aa87a..b394bc70b9 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@types/fs-extra": "^11.0.4", "@types/lodash": "^4.17.6", "@types/node": "^20.14.8", + "@types/semver": "^7.7.0", "@types/three": "^0.169.0", "@vitejs/plugin-vue": "^5.1.4", "@vue/test-utils": "^2.4.6", @@ -105,6 +106,7 @@ "pinia": "^2.1.7", "primeicons": "^7.0.0", "primevue": "^4.2.5", + "semver": "^7.7.2", "three": "^0.170.0", "tiptap-markdown": "^0.8.10", "vue": "^3.5.13", diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts index 3fd7daa306..dc687e4215 100644 --- a/src/stores/versionCompatibilityStore.ts +++ b/src/stores/versionCompatibilityStore.ts @@ -1,10 +1,10 @@ import { useStorage } from '@vueuse/core' import { defineStore } from 'pinia' +import * as semver from 'semver' import { computed } from 'vue' import config from '@/config' import { useSystemStatsStore } from '@/stores/systemStatsStore' -import { compareVersions, isSemVer } from '@/utils/formatUtil' const DISMISSAL_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days @@ -26,31 +26,69 @@ export const useVersionCompatibilityStore = defineStore( if ( !frontendVersion.value || !requiredFrontendVersion.value || - !isSemVer(frontendVersion.value) || - !isSemVer(requiredFrontendVersion.value) + !semver.valid(frontendVersion.value) || + !semver.valid(requiredFrontendVersion.value) ) { return false } - return ( - compareVersions(requiredFrontendVersion.value, frontendVersion.value) > - 0 - ) + // Returns true if required version is greater than frontend version + return semver.gt(requiredFrontendVersion.value, frontendVersion.value) }) const isFrontendNewer = computed(() => { + // Only check if all versions are valid semver if ( !frontendVersion.value || !backendVersion.value || - !isSemVer(frontendVersion.value) || - !isSemVer(backendVersion.value) + !semver.valid(frontendVersion.value) || + !semver.valid(backendVersion.value) ) { return false } - const versionDiff = compareVersions( - frontendVersion.value, - backendVersion.value - ) - return versionDiff > 0 + + // Check if frontend is newer than backend + if (!semver.gt(frontendVersion.value, backendVersion.value)) { + return false + } + + // If there's a required version specified by the backend + if ( + requiredFrontendVersion.value && + semver.valid(requiredFrontendVersion.value) + ) { + // If frontend version satisfies the required version, no warning needed + // Using satisfies allows for more flexible version matching (e.g., ^1.2.0, ~1.2.0) + // For exact version matching, we check if versions are within acceptable range + + // If frontend equals required version exactly, no warning + if (semver.eq(frontendVersion.value, requiredFrontendVersion.value)) { + return false + } + + // If frontend is behind required version, let isFrontendOutdated handle it + if (semver.lt(frontendVersion.value, requiredFrontendVersion.value)) { + return false + } + + // Frontend is ahead of required version - check if it's significantly ahead + const frontendMajor = semver.major(frontendVersion.value) + const frontendMinor = semver.minor(frontendVersion.value) + const requiredMajor = semver.major(requiredFrontendVersion.value) + const requiredMinor = semver.minor(requiredFrontendVersion.value) + + // If major versions differ, warn + if (frontendMajor !== requiredMajor) return true + + // If same major but more than 2 minor versions ahead, warn + if (frontendMinor - requiredMinor > 2) return true + + // Otherwise, frontend is reasonably close to required version, no warning + return false + } + + // No required version specified but frontend is newer than backend + // This is likely problematic, so warn + return true }) const hasVersionMismatch = computed(() => { diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index 5a59e0aee3..f8394e0a78 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -23,7 +23,14 @@ import { useBreakpoints, useEventListener } from '@vueuse/core' import type { ToastMessageOptions } from 'primevue/toast' import { useToast } from 'primevue/usetoast' -import { computed, onBeforeUnmount, onMounted, watch, watchEffect } from 'vue' +import { + computed, + nextTick, + onBeforeUnmount, + onMounted, + watch, + watchEffect +} from 'vue' import { useI18n } from 'vue-i18n' import MenuHamburger from '@/components/MenuHamburger.vue' @@ -233,6 +240,17 @@ const { wrapWithErrorHandling, wrapWithErrorHandlingAsync } = useErrorHandling() // It will be triggered automatically when the store is ready useFrontendVersionMismatchWarning({ immediate: true }) +// Initialize version compatibility check completely independently of app setup +// This runs asynchronously after component setup and won't block the main application +void nextTick(() => { + // Use setTimeout to ensure this happens after all other immediate tasks + setTimeout(() => { + versionCompatibilityStore.initialize().catch((error) => { + console.warn('Version compatibility check failed:', error) + }) + }, 100) // Small delay to ensure app is fully loaded +}) + const onGraphReady = () => { requestIdleCallback( () => { @@ -258,9 +276,6 @@ const onGraphReady = () => { // Explicitly initialize nodeSearchService to avoid indexing delay when // node search is triggered useNodeDefStore().nodeSearchService.searchNode('') - - // Initialize version compatibility store - void versionCompatibilityStore.initialize() }, { timeout: 1000 } ) diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts index c89027fd04..549c571e71 100644 --- a/tests-ui/tests/store/versionCompatibilityStore.test.ts +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -59,7 +59,9 @@ describe('useVersionCompatibilityStore', () => { expect(store.hasVersionMismatch).toBe(true) }) - it('should detect frontend is newer when frontend version is higher than backend', async () => { + it('should not warn when frontend is newer but meets required version', async () => { + // Frontend: 1.24.0, Backend: 1.23.0, Required: 1.23.0 + // Frontend is newer than backend but meets required version (only 1 minor version ahead) mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.23.0', @@ -70,8 +72,8 @@ describe('useVersionCompatibilityStore', () => { await store.checkVersionCompatibility() expect(store.isFrontendOutdated).toBe(false) - expect(store.isFrontendNewer).toBe(true) - expect(store.hasVersionMismatch).toBe(true) + expect(store.isFrontendNewer).toBe(false) // Should NOT warn + expect(store.hasVersionMismatch).toBe(false) }) it('should not detect mismatch when versions are compatible', async () => { @@ -108,7 +110,7 @@ describe('useVersionCompatibilityStore', () => { mockSystemStatsStore.systemStats = { system: { comfyui_version: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be', // git hash - required_frontend_version: '1.29.2.45' // invalid semver + required_frontend_version: 'not-a-version' // invalid semver format } } @@ -118,6 +120,70 @@ describe('useVersionCompatibilityStore', () => { expect(store.isFrontendNewer).toBe(false) expect(store.hasVersionMismatch).toBe(false) }) + + it('should not warn when frontend is ahead of required version within acceptable range', async () => { + // Frontend: 1.24.0 (from mock config) + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.22.0', // Backend is older + required_frontend_version: '1.23.0' // Required is 1.23.0, frontend 1.24.0 meets this + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) // Frontend 1.24.0 >= Required 1.23.0 + expect(store.isFrontendNewer).toBe(false) // Should NOT warn - frontend meets requirements + expect(store.hasVersionMismatch).toBe(false) + }) + + it('should warn when frontend is significantly ahead of required version', async () => { + // Frontend: 1.24.0 (from mock config) + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.20.0', // Backend is much older + required_frontend_version: '1.20.0' // Required is 1.20.0, frontend 1.24.0 is 4 minor versions ahead + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(true) // Should warn - frontend is too far ahead + expect(store.hasVersionMismatch).toBe(true) + }) + + it('should warn when frontend major version differs from required', async () => { + // Frontend: 1.24.0 (from mock config) + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '0.9.0', // Backend is on different major version + required_frontend_version: '0.9.0' // Required is 0.9.0, frontend 1.24.0 is different major + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(true) // Should warn - major version mismatch + expect(store.hasVersionMismatch).toBe(true) + }) + + it('should warn when frontend is newer and no required version specified', async () => { + // Frontend: 1.24.0 (from mock config) + mockSystemStatsStore.systemStats = { + system: { + comfyui_version: '1.20.0', // Backend is older + required_frontend_version: '' // No required version specified + } + } + + await store.checkVersionCompatibility() + + expect(store.isFrontendOutdated).toBe(false) + expect(store.isFrontendNewer).toBe(true) // Should warn - no required version to check against + expect(store.hasVersionMismatch).toBe(true) + }) }) describe('warning display logic', () => { @@ -187,11 +253,13 @@ describe('useVersionCompatibilityStore', () => { }) }) - it('should generate newer message when frontend is newer', async () => { + it('should generate newer message when frontend is significantly newer', async () => { + // Frontend: 1.24.0, Backend: 1.20.0, Required: 1.20.0 + // Frontend is 4 minor versions ahead - should warn mockSystemStatsStore.systemStats = { system: { - comfyui_version: '1.23.0', - required_frontend_version: '1.23.0' + comfyui_version: '1.20.0', + required_frontend_version: '1.20.0' } } @@ -200,7 +268,7 @@ describe('useVersionCompatibilityStore', () => { expect(store.warningMessage).toEqual({ type: 'newer', frontendVersion: '1.24.0', - backendVersion: '1.23.0' + backendVersion: '1.20.0' }) }) From ad5500ee8265a06d7861ca8135495e9133298267 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 26 Jul 2025 22:57:41 -0700 Subject: [PATCH 10/12] [bugfix] Simplify version warning logic to only warn when outdated - Remove isFrontendNewer logic - only warn when required version > installed version - Simplify warningMessage computed to only handle outdated case - Update tests to reflect the simplified behavior - Clean up unnecessary complexity in version comparison The warning system now only shows when the frontend is behind the required version, which is the only case where user action is needed. Co-Authored-By: Claude --- .../useFrontendVersionMismatchWarning.ts | 14 +--- src/stores/versionCompatibilityStore.ts | 64 +-------------- .../useFrontendVersionMismatchWarning.test.ts | 23 ------ .../store/versionCompatibilityStore.test.ts | 77 ++----------------- 4 files changed, 13 insertions(+), 165 deletions(-) diff --git a/src/composables/useFrontendVersionMismatchWarning.ts b/src/composables/useFrontendVersionMismatchWarning.ts index 676cd58a56..11897a0162 100644 --- a/src/composables/useFrontendVersionMismatchWarning.ts +++ b/src/composables/useFrontendVersionMismatchWarning.ts @@ -48,16 +48,10 @@ export function useFrontendVersionMismatchWarning( const message = versionCompatibilityStore.warningMessage if (!message) return - const detailMessage = - message.type === 'outdated' - ? t('g.frontendOutdated', { - frontendVersion: message.frontendVersion, - requiredVersion: message.requiredVersion - }) - : t('g.frontendNewer', { - frontendVersion: message.frontendVersion, - backendVersion: message.backendVersion - }) + const detailMessage = t('g.frontendOutdated', { + frontendVersion: message.frontendVersion, + requiredVersion: message.requiredVersion + }) const fullMessage = t('g.versionMismatchWarningMessage', { warning: t('g.versionMismatchWarning'), diff --git a/src/stores/versionCompatibilityStore.ts b/src/stores/versionCompatibilityStore.ts index dc687e4215..da483846e3 100644 --- a/src/stores/versionCompatibilityStore.ts +++ b/src/stores/versionCompatibilityStore.ts @@ -36,63 +36,13 @@ export const useVersionCompatibilityStore = defineStore( }) const isFrontendNewer = computed(() => { - // Only check if all versions are valid semver - if ( - !frontendVersion.value || - !backendVersion.value || - !semver.valid(frontendVersion.value) || - !semver.valid(backendVersion.value) - ) { - return false - } - - // Check if frontend is newer than backend - if (!semver.gt(frontendVersion.value, backendVersion.value)) { - return false - } - - // If there's a required version specified by the backend - if ( - requiredFrontendVersion.value && - semver.valid(requiredFrontendVersion.value) - ) { - // If frontend version satisfies the required version, no warning needed - // Using satisfies allows for more flexible version matching (e.g., ^1.2.0, ~1.2.0) - // For exact version matching, we check if versions are within acceptable range - - // If frontend equals required version exactly, no warning - if (semver.eq(frontendVersion.value, requiredFrontendVersion.value)) { - return false - } - - // If frontend is behind required version, let isFrontendOutdated handle it - if (semver.lt(frontendVersion.value, requiredFrontendVersion.value)) { - return false - } - - // Frontend is ahead of required version - check if it's significantly ahead - const frontendMajor = semver.major(frontendVersion.value) - const frontendMinor = semver.minor(frontendVersion.value) - const requiredMajor = semver.major(requiredFrontendVersion.value) - const requiredMinor = semver.minor(requiredFrontendVersion.value) - - // If major versions differ, warn - if (frontendMajor !== requiredMajor) return true - - // If same major but more than 2 minor versions ahead, warn - if (frontendMinor - requiredMinor > 2) return true - - // Otherwise, frontend is reasonably close to required version, no warning - return false - } - - // No required version specified but frontend is newer than backend - // This is likely problematic, so warn - return true + // We don't warn about frontend being newer than backend + // Only warn when frontend is outdated (behind required version) + return false }) const hasVersionMismatch = computed(() => { - return isFrontendOutdated.value || isFrontendNewer.value + return isFrontendOutdated.value }) const versionKey = computed(() => { @@ -147,12 +97,6 @@ export const useVersionCompatibilityStore = defineStore( frontendVersion: frontendVersion.value, requiredVersion: requiredFrontendVersion.value } - } else if (isFrontendNewer.value) { - return { - type: 'newer' as const, - frontendVersion: frontendVersion.value, - backendVersion: backendVersion.value - } } return null }) diff --git a/tests-ui/tests/composables/useFrontendVersionMismatchWarning.test.ts b/tests-ui/tests/composables/useFrontendVersionMismatchWarning.test.ts index b4ed7c2634..b8b4fceac7 100644 --- a/tests-ui/tests/composables/useFrontendVersionMismatchWarning.test.ts +++ b/tests-ui/tests/composables/useFrontendVersionMismatchWarning.test.ts @@ -153,29 +153,6 @@ describe('useFrontendVersionMismatchWarning', () => { expect(addAlertSpy).toHaveBeenCalledOnce() }) - it('should show warning for newer frontend version', async () => { - const toastStore = useToastStore() - const versionStore = useVersionCompatibilityStore() - const addAlertSpy = vi.spyOn(toastStore, 'addAlert') - const dismissWarningSpy = vi.spyOn(versionStore, 'dismissWarning') - - // Mock version mismatch with newer frontend - vi.spyOn(versionStore, 'shouldShowWarning', 'get').mockReturnValue(true) - vi.spyOn(versionStore, 'warningMessage', 'get').mockReturnValue({ - type: 'newer', - frontendVersion: '2.0.0', - backendVersion: '1.0.0' - }) - - useFrontendVersionMismatchWarning({ immediate: true }) - await nextTick() - - expect(addAlertSpy).toHaveBeenCalledWith( - expect.stringContaining('Frontend version 2.0.0 may not be compatible') - ) - expect(dismissWarningSpy).toHaveBeenCalled() - }) - it('should call showWarning method manually', () => { const toastStore = useToastStore() const versionStore = useVersionCompatibilityStore() diff --git a/tests-ui/tests/store/versionCompatibilityStore.test.ts b/tests-ui/tests/store/versionCompatibilityStore.test.ts index 549c571e71..e3d3ceca99 100644 --- a/tests-ui/tests/store/versionCompatibilityStore.test.ts +++ b/tests-ui/tests/store/versionCompatibilityStore.test.ts @@ -59,9 +59,9 @@ describe('useVersionCompatibilityStore', () => { expect(store.hasVersionMismatch).toBe(true) }) - it('should not warn when frontend is newer but meets required version', async () => { + it('should not warn when frontend is newer than backend', async () => { // Frontend: 1.24.0, Backend: 1.23.0, Required: 1.23.0 - // Frontend is newer than backend but meets required version (only 1 minor version ahead) + // Frontend meets required version, no warning needed mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.23.0', @@ -72,7 +72,7 @@ describe('useVersionCompatibilityStore', () => { await store.checkVersionCompatibility() expect(store.isFrontendOutdated).toBe(false) - expect(store.isFrontendNewer).toBe(false) // Should NOT warn + expect(store.isFrontendNewer).toBe(false) expect(store.hasVersionMismatch).toBe(false) }) @@ -121,7 +121,7 @@ describe('useVersionCompatibilityStore', () => { expect(store.hasVersionMismatch).toBe(false) }) - it('should not warn when frontend is ahead of required version within acceptable range', async () => { + it('should not warn when frontend exceeds required version', async () => { // Frontend: 1.24.0 (from mock config) mockSystemStatsStore.systemStats = { system: { @@ -133,57 +133,9 @@ describe('useVersionCompatibilityStore', () => { await store.checkVersionCompatibility() expect(store.isFrontendOutdated).toBe(false) // Frontend 1.24.0 >= Required 1.23.0 - expect(store.isFrontendNewer).toBe(false) // Should NOT warn - frontend meets requirements + expect(store.isFrontendNewer).toBe(false) // Never warns about being newer expect(store.hasVersionMismatch).toBe(false) }) - - it('should warn when frontend is significantly ahead of required version', async () => { - // Frontend: 1.24.0 (from mock config) - mockSystemStatsStore.systemStats = { - system: { - comfyui_version: '1.20.0', // Backend is much older - required_frontend_version: '1.20.0' // Required is 1.20.0, frontend 1.24.0 is 4 minor versions ahead - } - } - - await store.checkVersionCompatibility() - - expect(store.isFrontendOutdated).toBe(false) - expect(store.isFrontendNewer).toBe(true) // Should warn - frontend is too far ahead - expect(store.hasVersionMismatch).toBe(true) - }) - - it('should warn when frontend major version differs from required', async () => { - // Frontend: 1.24.0 (from mock config) - mockSystemStatsStore.systemStats = { - system: { - comfyui_version: '0.9.0', // Backend is on different major version - required_frontend_version: '0.9.0' // Required is 0.9.0, frontend 1.24.0 is different major - } - } - - await store.checkVersionCompatibility() - - expect(store.isFrontendOutdated).toBe(false) - expect(store.isFrontendNewer).toBe(true) // Should warn - major version mismatch - expect(store.hasVersionMismatch).toBe(true) - }) - - it('should warn when frontend is newer and no required version specified', async () => { - // Frontend: 1.24.0 (from mock config) - mockSystemStatsStore.systemStats = { - system: { - comfyui_version: '1.20.0', // Backend is older - required_frontend_version: '' // No required version specified - } - } - - await store.checkVersionCompatibility() - - expect(store.isFrontendOutdated).toBe(false) - expect(store.isFrontendNewer).toBe(true) // Should warn - no required version to check against - expect(store.hasVersionMismatch).toBe(true) - }) }) describe('warning display logic', () => { @@ -253,25 +205,6 @@ describe('useVersionCompatibilityStore', () => { }) }) - it('should generate newer message when frontend is significantly newer', async () => { - // Frontend: 1.24.0, Backend: 1.20.0, Required: 1.20.0 - // Frontend is 4 minor versions ahead - should warn - mockSystemStatsStore.systemStats = { - system: { - comfyui_version: '1.20.0', - required_frontend_version: '1.20.0' - } - } - - await store.checkVersionCompatibility() - - expect(store.warningMessage).toEqual({ - type: 'newer', - frontendVersion: '1.24.0', - backendVersion: '1.20.0' - }) - }) - it('should return null when no mismatch', async () => { mockSystemStatsStore.systemStats = { system: { From aee5f8f13493bebcb449cbf183fda80890eb1b47 Mon Sep 17 00:00:00 2001 From: bymyself Date: Mon, 28 Jul 2025 15:28:28 -0700 Subject: [PATCH 11/12] add browser test --- .../tests/versionMismatchWarnings.spec.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 browser_tests/tests/versionMismatchWarnings.spec.ts diff --git a/browser_tests/tests/versionMismatchWarnings.spec.ts b/browser_tests/tests/versionMismatchWarnings.spec.ts new file mode 100644 index 0000000000..d85f187238 --- /dev/null +++ b/browser_tests/tests/versionMismatchWarnings.spec.ts @@ -0,0 +1,117 @@ +import { expect } from '@playwright/test' + +import { SystemStats } from '../../src/schemas/apiSchema' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.describe('Version Mismatch Warnings', () => { + const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100' + const ALWAYS_BEHIND_INSTALLED_VERSION = '0.0.0' + + const createMockSystemStatsRes = ( + requiredFrontendVersion: string + ): SystemStats => { + return { + system: { + os: 'posix', + ram_total: 67235385344, + ram_free: 13464207360, + comfyui_version: '0.3.46', + required_frontend_version: requiredFrontendVersion, + python_version: '3.12.3 (main, Jun 18 2025, 17:59:45) [GCC 13.3.0]', + pytorch_version: '2.6.0+cu124', + embedded_python: false, + argv: ['main.py'] + }, + devices: [ + { + name: 'cuda:0 NVIDIA GeForce RTX 4070 : cudaMallocAsync', + type: 'cuda', + index: 0, + vram_total: 12557156352, + vram_free: 2439249920, + torch_vram_total: 0, + torch_vram_free: 0 + } + ] + } + } + + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + }) + + test('should show version mismatch warnings when installed version lower than required', async ({ + comfyPage + }) => { + // Mock system_stats route to indicate that the installed version is always ahead of the required version + await comfyPage.page.route('**/system_stats**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify( + createMockSystemStatsRes(ALWAYS_AHEAD_OF_INSTALLED_VERSION) + ) + }) + }) + await comfyPage.setup() + + // Expect a warning toast to be shown + await expect( + comfyPage.page.getByText('Version Compatibility Warning') + ).toBeVisible() + }) + + test('should not show version mismatch warnings when installed version is ahead of required', async ({ + comfyPage + }) => { + // Mock system_stats route to indicate that the installed version is always ahead of the required version + await comfyPage.page.route('**/system_stats**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify( + createMockSystemStatsRes(ALWAYS_BEHIND_INSTALLED_VERSION) + ) + }) + }) + await comfyPage.setup() + + // Expect no warning toast to be shown + await expect( + comfyPage.page.getByText('Version Compatibility Warning') + ).not.toBeVisible() + }) + + test('should persist dismissed state across sessions', async ({ + comfyPage + }) => { + // Mock system_stats route to indicate that the installed version is always ahead of the required version + await comfyPage.page.route('**/system_stats**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify( + createMockSystemStatsRes(ALWAYS_AHEAD_OF_INSTALLED_VERSION) + ) + }) + }) + await comfyPage.setup() + + // Locate the warning toast and dismiss it + const warningToast = comfyPage.page + .locator('div') + .filter({ hasText: 'Version Compatibility' }) + .nth(3) + await warningToast.waitFor({ state: 'visible' }) + const dismissButton = warningToast.getByRole('button', { name: 'Close' }) + await dismissButton.click() + + // Reload the page, keeping local storage + await comfyPage.setup({ clearStorage: false }) + + // The same warning from same versions should not be shown to the user again + await expect( + comfyPage.page.getByText('Version Compatibility Warning') + ).not.toBeVisible() + }) +}) From 770b49d6040b7c6c3ccd711d0f4783248e2be92f Mon Sep 17 00:00:00 2001 From: bymyself Date: Mon, 28 Jul 2025 16:33:47 -0700 Subject: [PATCH 12/12] [cleanup] Remove unused version mismatch dismissal setting and unnecessary code - Remove unused Comfy.VersionMismatch.DismissedVersion setting and schema - Remove unnecessary setTimeout wrapper in version compatibility initialization - Remove redundant comments about version compatibility setup The dismissal mechanism now uses localStorage directly via useStorage. --- src/constants/coreSettings.ts | 6 ------ src/schemas/apiSchema.ts | 2 -- src/views/GraphView.vue | 11 +++-------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index 4fca5aac2f..c3a9b30af8 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -884,11 +884,5 @@ export const CORE_SETTINGS: SettingParams[] = [ name: 'Release seen timestamp', type: 'hidden', defaultValue: 0 - }, - { - id: 'Comfy.VersionMismatch.DismissedVersion', - name: 'Dismissed version mismatch warning', - type: 'hidden', - defaultValue: '' } ] diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 0594721c6b..f2ad92e972 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -486,8 +486,6 @@ const zSettings = z.object({ "what's new seen" ]), 'Comfy.Release.Timestamp': z.number(), - /** Version compatibility settings */ - 'Comfy.VersionMismatch.DismissedVersion': z.string(), /** Settings used for testing */ 'test.setting': z.any(), 'main.sub.setting.name': z.any(), diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index f8394e0a78..f5e2992944 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -240,15 +240,10 @@ const { wrapWithErrorHandling, wrapWithErrorHandlingAsync } = useErrorHandling() // It will be triggered automatically when the store is ready useFrontendVersionMismatchWarning({ immediate: true }) -// Initialize version compatibility check completely independently of app setup -// This runs asynchronously after component setup and won't block the main application void nextTick(() => { - // Use setTimeout to ensure this happens after all other immediate tasks - setTimeout(() => { - versionCompatibilityStore.initialize().catch((error) => { - console.warn('Version compatibility check failed:', error) - }) - }, 100) // Small delay to ensure app is fully loaded + versionCompatibilityStore.initialize().catch((error) => { + console.warn('Version compatibility check failed:', error) + }) }) const onGraphReady = () => {