From 6e8a9ae41fe6a93e3d43341e0f0631b8baae6bf9 Mon Sep 17 00:00:00 2001 From: bmcomfy Date: Tue, 8 Jul 2025 14:43:11 -0700 Subject: [PATCH] [System Pop Up] Add setting to disable version update notifications (#4388) --- .../tests/releaseNotifications.spec.ts | 235 ++++++++++++++++++ .../helpcenter/HelpCenterMenuContent.vue | 7 +- src/constants/coreSettings.ts | 8 + src/locales/en/settings.json | 4 + src/schemas/apiSchema.ts | 1 + src/stores/releaseStore.ts | 29 ++- tests-ui/tests/store/releaseStore.test.ts | 162 +++++++++++- 7 files changed, 441 insertions(+), 5 deletions(-) diff --git a/browser_tests/tests/releaseNotifications.spec.ts b/browser_tests/tests/releaseNotifications.spec.ts index 5e5e58001..19d09327d 100644 --- a/browser_tests/tests/releaseNotifications.spec.ts +++ b/browser_tests/tests/releaseNotifications.spec.ts @@ -130,4 +130,239 @@ test.describe('Release Notifications', () => { whatsNewSection.locator('text=No recent releases') ).toBeVisible() }) + + test('should hide "What\'s New" section when notifications are disabled', async ({ + comfyPage + }) => { + // Disable version update notifications + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + + // Mock release API with test data + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 1, + project: 'comfyui', + version: 'v0.3.44', + attention: 'high', + content: '## New Features\n\n- Added awesome feature', + published_at: new Date().toISOString() + } + ]) + }) + } else { + await route.continue() + } + }) + + await comfyPage.setup({ mockReleases: false }) + + // Open help center + const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn') + await helpCenterButton.waitFor({ state: 'visible' }) + await helpCenterButton.click() + + // Verify help center menu appears + const helpMenu = comfyPage.page.locator('.help-center-menu') + await expect(helpMenu).toBeVisible() + + // Verify "What's New?" section is hidden + const whatsNewSection = comfyPage.page.locator('.whats-new-section') + await expect(whatsNewSection).not.toBeVisible() + + // Should not show any popups or toasts + await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible() + await expect( + comfyPage.page.locator('.release-notification-toast') + ).not.toBeVisible() + }) + + test('should not make API calls when notifications are disabled', async ({ + comfyPage + }) => { + // Disable version update notifications + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + + // Track API calls + let apiCallCount = 0 + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + apiCallCount++ + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([]) + }) + } else { + await route.continue() + } + }) + + await comfyPage.setup({ mockReleases: false }) + + // Wait a bit to ensure any potential API calls would have been made + await comfyPage.page.waitForTimeout(1000) + + // Verify no API calls were made + expect(apiCallCount).toBe(0) + }) + + test('should show "What\'s New" section when notifications are enabled', async ({ + comfyPage + }) => { + // Enable version update notifications (default behavior) + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true) + + // Mock release API with test data + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 1, + project: 'comfyui', + version: 'v0.3.44', + attention: 'medium', + content: '## New Features\n\n- Added awesome feature', + published_at: new Date().toISOString() + } + ]) + }) + } else { + await route.continue() + } + }) + + await comfyPage.setup({ mockReleases: false }) + + // Open help center + const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn') + await helpCenterButton.waitFor({ state: 'visible' }) + await helpCenterButton.click() + + // Verify help center menu appears + const helpMenu = comfyPage.page.locator('.help-center-menu') + await expect(helpMenu).toBeVisible() + + // Verify "What's New?" section is visible + const whatsNewSection = comfyPage.page.locator('.whats-new-section') + await expect(whatsNewSection).toBeVisible() + + // Should show the release + await expect( + whatsNewSection.locator('text=Comfy v0.3.44 Release') + ).toBeVisible() + }) + + test('should toggle "What\'s New" section when setting changes', async ({ + comfyPage + }) => { + // Mock release API with test data + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 1, + project: 'comfyui', + version: 'v0.3.44', + attention: 'low', + content: '## Bug Fixes\n\n- Fixed minor issue', + published_at: new Date().toISOString() + } + ]) + }) + } else { + await route.continue() + } + }) + + // Start with notifications enabled + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true) + await comfyPage.setup({ mockReleases: false }) + + // Open help center + const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn') + await helpCenterButton.waitFor({ state: 'visible' }) + await helpCenterButton.click() + + // Verify "What's New?" section is visible + const whatsNewSection = comfyPage.page.locator('.whats-new-section') + await expect(whatsNewSection).toBeVisible() + + // Close help center + await comfyPage.page.click('.help-center-backdrop') + + // Disable notifications + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + + // Reopen help center + await helpCenterButton.click() + + // Verify "What's New?" section is now hidden + await expect(whatsNewSection).not.toBeVisible() + }) + + test('should handle edge case with empty releases and disabled notifications', async ({ + comfyPage + }) => { + // Disable notifications + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + + // Mock empty releases + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([]) + }) + } else { + await route.continue() + } + }) + + await comfyPage.setup({ mockReleases: false }) + + // Open help center + const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn') + await helpCenterButton.waitFor({ state: 'visible' }) + await helpCenterButton.click() + + // Verify help center still works + const helpMenu = comfyPage.page.locator('.help-center-menu') + await expect(helpMenu).toBeVisible() + + // Section should be hidden regardless of empty releases + const whatsNewSection = comfyPage.page.locator('.whats-new-section') + await expect(whatsNewSection).not.toBeVisible() + }) }) diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue index c9f708937..f5dad4df1 100644 --- a/src/components/helpcenter/HelpCenterMenuContent.vue +++ b/src/components/helpcenter/HelpCenterMenuContent.vue @@ -54,7 +54,7 @@ -
+

{{ $t('helpCenter.whatsNew') }}

@@ -126,6 +126,7 @@ import { useI18n } from 'vue-i18n' import { type ReleaseNote } from '@/services/releaseService' import { useCommandStore } from '@/stores/commandStore' import { useReleaseStore } from '@/stores/releaseStore' +import { useSettingStore } from '@/stores/settingStore' import { electronAPI, isElectron } from '@/utils/envUtil' import { formatVersionAnchor } from '@/utils/formatUtil' @@ -168,6 +169,7 @@ const SUBMENU_CONFIG = { const { t, locale } = useI18n() const releaseStore = useReleaseStore() const commandStore = useCommandStore() +const settingStore = useSettingStore() // Emits const emit = defineEmits<{ @@ -182,6 +184,9 @@ let hoverTimeout: number | null = null // Computed const hasReleases = computed(() => releaseStore.releases.length > 0) +const showVersionUpdates = computed(() => + settingStore.get('Comfy.Notification.ShowVersionUpdates') +) const moreMenuItem = computed(() => menuItems.value.find((item) => item.key === 'more') diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index d71e329a5..312d8d5fa 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -330,6 +330,14 @@ export const CORE_SETTINGS: SettingParams[] = [ defaultValue: true, versionAdded: '1.20.3' }, + { + id: 'Comfy.Notification.ShowVersionUpdates', + category: ['Comfy', 'Notification Preferences'], + name: 'Show version updates', + tooltip: 'Show updates for new models, and major new features.', + type: 'boolean', + defaultValue: true + }, { id: 'Comfy.ConfirmClear', category: ['Comfy', 'Workflow', 'ConfirmClear'], diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index ffe13f225..1c39b4e37 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -232,6 +232,10 @@ "Comfy_NodeBadge_ShowApiPricing": { "name": "Show API node pricing badge" }, + "Comfy_Notification_ShowVersionUpdates": { + "name": "Show version updates", + "tooltip": "Show updates for new models, and major new features." + }, "Comfy_NodeSearchBoxImpl": { "name": "Node search box implementation", "options": { diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 4d436560a..91597af60 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -426,6 +426,7 @@ const zSettings = z.object({ 'Comfy.NodeBadge.NodeIdBadgeMode': zNodeBadgeMode, 'Comfy.NodeBadge.NodeLifeCycleBadgeMode': zNodeBadgeMode, 'Comfy.NodeBadge.ShowApiPricing': z.boolean(), + 'Comfy.Notification.ShowVersionUpdates': z.boolean(), 'Comfy.QueueButton.BatchCountLimit': z.number(), 'Comfy.Queue.MaxHistoryItems': z.number(), 'Comfy.Keybinding.UnsetBindings': z.array(zKeybinding), diff --git a/src/stores/releaseStore.ts b/src/stores/releaseStore.ts index c95a9c957..321204d9a 100644 --- a/src/stores/releaseStore.ts +++ b/src/stores/releaseStore.ts @@ -32,6 +32,9 @@ export const useReleaseStore = defineStore('release', () => { const releaseTimestamp = computed(() => settingStore.get('Comfy.Release.Timestamp') ) + const showVersionUpdates = computed(() => + settingStore.get('Comfy.Notification.ShowVersionUpdates') + ) // Most recent release const recentRelease = computed(() => { @@ -73,6 +76,11 @@ export const useReleaseStore = defineStore('release', () => { // Show toast if needed const shouldShowToast = computed(() => { + // Skip if notifications are disabled + if (!showVersionUpdates.value) { + return false + } + if (!isNewVersionAvailable.value) { return false } @@ -85,7 +93,7 @@ export const useReleaseStore = defineStore('release', () => { // Skip if user already skipped or changelog seen if ( releaseVersion.value === recentRelease.value?.version && - !['skipped', 'changelog seen'].includes(releaseStatus.value) + ['skipped', 'changelog seen'].includes(releaseStatus.value) ) { return false } @@ -95,6 +103,11 @@ export const useReleaseStore = defineStore('release', () => { // Show red-dot indicator const shouldShowRedDot = computed(() => { + // Skip if notifications are disabled + if (!showVersionUpdates.value) { + return false + } + // Already latest → no dot if (!isNewVersionAvailable.value) { return false @@ -132,6 +145,11 @@ export const useReleaseStore = defineStore('release', () => { // Show "What's New" popup const shouldShowPopup = computed(() => { + // Skip if notifications are disabled + if (!showVersionUpdates.value) { + return false + } + if (!isLatestVersion.value) { return false } @@ -183,7 +201,14 @@ export const useReleaseStore = defineStore('release', () => { // Fetch releases from API async function fetchReleases(): Promise { - if (isLoading.value) return + if (isLoading.value) { + return + } + + // Skip fetching if notifications are disabled + if (!showVersionUpdates.value) { + return + } isLoading.value = true error.value = null diff --git a/tests-ui/tests/store/releaseStore.test.ts b/tests-ui/tests/store/releaseStore.test.ts index 99c826cfa..940c575ba 100644 --- a/tests-ui/tests/store/releaseStore.test.ts +++ b/tests-ui/tests/store/releaseStore.test.ts @@ -61,6 +61,12 @@ describe('useReleaseStore', () => { vi.mocked(useSettingStore).mockReturnValue(mockSettingStore) vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore) + // Default showVersionUpdates to true + mockSettingStore.get.mockImplementation((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return true + return null + }) + store = useReleaseStore() }) @@ -114,6 +120,107 @@ describe('useReleaseStore', () => { }) }) + describe('showVersionUpdates setting', () => { + beforeEach(() => { + store.releases = [mockRelease] + }) + + describe('when notifications are enabled', () => { + beforeEach(() => { + mockSettingStore.get.mockImplementation((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return true + return null + }) + }) + + it('should show toast for medium/high attention releases', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(1) + + // Need multiple releases for hasMediumOrHighAttention to work + const mediumRelease = { + ...mockRelease, + id: 2, + attention: 'medium' as const + } + store.releases = [mockRelease, mediumRelease] + + expect(store.shouldShowToast).toBe(true) + }) + + it('should show red dot for new versions', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(1) + + expect(store.shouldShowRedDot).toBe(true) + }) + + it('should show popup for latest version', async () => { + mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(0) + + expect(store.shouldShowPopup).toBe(true) + }) + + it('should fetch releases during initialization', async () => { + mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + + await store.initialize() + + expect(mockReleaseService.getReleases).toHaveBeenCalledWith({ + project: 'comfyui', + current_version: '1.0.0', + form_factor: 'git-windows' + }) + }) + }) + + describe('when notifications are disabled', () => { + beforeEach(() => { + mockSettingStore.get.mockImplementation((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return false + return null + }) + }) + + it('should not show toast even with new version available', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(1) + + expect(store.shouldShowToast).toBe(false) + }) + + it('should not show red dot even with new version available', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(1) + + expect(store.shouldShowRedDot).toBe(false) + }) + + it('should not show popup even for latest version', async () => { + mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(0) + + expect(store.shouldShowPopup).toBe(false) + }) + + it('should skip fetching releases during initialization', async () => { + await store.initialize() + + expect(mockReleaseService.getReleases).not.toHaveBeenCalled() + }) + + it('should not fetch releases when calling fetchReleases directly', async () => { + await store.fetchReleases() + + expect(mockReleaseService.getReleases).not.toHaveBeenCalled() + expect(store.isLoading).toBe(false) + }) + }) + }) + describe('release initialization', () => { it('should fetch releases successfully', async () => { mockReleaseService.getReleases.mockResolvedValue([mockRelease]) @@ -184,6 +291,17 @@ describe('useReleaseStore', () => { expect(mockSystemStatsStore.fetchSystemStats).toHaveBeenCalled() }) + + it('should not set loading state when notifications disabled', async () => { + mockSettingStore.get.mockImplementation((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return false + return null + }) + + await store.initialize() + + expect(store.isLoading).toBe(false) + }) }) describe('action handlers', () => { @@ -248,6 +366,7 @@ describe('useReleaseStore', () => { mockSettingStore.get.mockImplementation((key: string) => { if (key === 'Comfy.Release.Version') return null if (key === 'Comfy.Release.Status') return null + if (key === 'Comfy.Notification.ShowVersionUpdates') return true return null }) @@ -267,7 +386,10 @@ describe('useReleaseStore', () => { it('should show red dot for new versions', async () => { const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(compareVersions).mockReturnValue(1) - mockSettingStore.get.mockReturnValue(null) + mockSettingStore.get.mockImplementation((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return true + return null + }) store.releases = [mockRelease] @@ -276,7 +398,10 @@ describe('useReleaseStore', () => { it('should show popup for latest version', async () => { mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' // Same as release - mockSettingStore.get.mockReturnValue(null) + mockSettingStore.get.mockImplementation((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return true + return null + }) const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(compareVersions).mockReturnValue(0) // versions are equal (latest version) @@ -286,4 +411,37 @@ describe('useReleaseStore', () => { expect(store.shouldShowPopup).toBe(true) }) }) + + describe('edge cases', () => { + it('should handle missing system stats gracefully', async () => { + mockSystemStatsStore.systemStats = null + mockSettingStore.get.mockImplementation((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return false + return null + }) + + await store.initialize() + + // Should not fetch system stats when notifications disabled + expect(mockSystemStatsStore.fetchSystemStats).not.toHaveBeenCalled() + }) + + it('should handle concurrent fetchReleases calls', async () => { + mockReleaseService.getReleases.mockImplementation( + () => + new Promise((resolve) => + setTimeout(() => resolve([mockRelease]), 100) + ) + ) + + // Start two concurrent calls + const promise1 = store.fetchReleases() + const promise2 = store.fetchReleases() + + await Promise.all([promise1, promise2]) + + // Should only call API once due to loading check + expect(mockReleaseService.getReleases).toHaveBeenCalledTimes(1) + }) + }) })