[fix] Improve version mismatch warning dismissal and eliminate code duplication (#4542)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-07-28 15:35:17 -07:00
committed by GitHub
parent 6a36557c69
commit 71b5b285e1
10 changed files with 662 additions and 350 deletions

View File

@@ -0,0 +1,234 @@
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<typeof import('vue')>()
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 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)
})
})

View File

@@ -1,7 +1,7 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { useVersionCompatibilityStore } from '@/stores/versionCompatibilityStore'
@@ -12,32 +12,37 @@ vi.mock('@/config', () => ({
}))
vi.mock('@/stores/systemStatsStore')
vi.mock('@/stores/settingStore')
// Mock useStorage from VueUse
const mockDismissalStorage = ref({} as Record<string, number>)
vi.mock('@vueuse/core', () => ({
useStorage: vi.fn(() => mockDismissalStorage)
}))
describe('useVersionCompatibilityStore', () => {
let store: ReturnType<typeof useVersionCompatibilityStore>
let mockSystemStatsStore: any
let mockSettingStore: any
beforeEach(() => {
setActivePinia(createPinia())
// Clear the mock dismissal storage
mockDismissalStorage.value = {}
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 = {
@@ -54,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 than backend', async () => {
// Frontend: 1.24.0, Backend: 1.23.0, Required: 1.23.0
// Frontend meets required version, no warning needed
mockSystemStatsStore.systemStats = {
system: {
comfyui_version: '1.23.0',
@@ -65,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)
expect(store.hasVersionMismatch).toBe(false)
})
it('should not detect mismatch when versions are compatible', async () => {
@@ -103,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
}
}
@@ -113,14 +120,28 @@ describe('useVersionCompatibilityStore', () => {
expect(store.isFrontendNewer).toBe(false)
expect(store.hasVersionMismatch).toBe(false)
})
it('should not warn when frontend exceeds required version', 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) // Never warns about being newer
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 () => {
// No dismissals in storage
mockDismissalStorage.value = {}
mockSystemStatsStore.systemStats = {
system: {
comfyui_version: '1.25.0',
@@ -134,6 +155,12 @@ describe('useVersionCompatibilityStore', () => {
})
it('should not show warning when dismissed', async () => {
const futureTime = Date.now() + 1000000
// Set dismissal in reactive storage
mockDismissalStorage.value = {
'1.24.0-1.25.0-1.25.0': futureTime
}
mockSystemStatsStore.systemStats = {
system: {
comfyui_version: '1.25.0',
@@ -142,7 +169,6 @@ describe('useVersionCompatibilityStore', () => {
}
await store.checkVersionCompatibility()
void store.dismissWarning()
expect(store.shouldShowWarning).toBe(false)
})
@@ -179,23 +205,6 @@ describe('useVersionCompatibilityStore', () => {
})
})
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: {
@@ -211,7 +220,10 @@ describe('useVersionCompatibilityStore', () => {
})
describe('dismissal persistence', () => {
it('should save dismissal to settings', async () => {
it('should save dismissal to reactive storage with expiration', async () => {
const mockNow = 1000000
vi.spyOn(Date, 'now').mockReturnValue(mockNow)
mockSystemStatsStore.systemStats = {
system: {
comfyui_version: '1.25.0',
@@ -220,16 +232,20 @@ 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'
)
// 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 restore dismissal state from settings', async () => {
mockSettingStore.get.mockReturnValue('1.24.0-1.25.0-1.25.0')
it('should check dismissal state from reactive storage', async () => {
const futureTime = Date.now() + 1000000 // Still valid
mockDismissalStorage.value = {
'1.24.0-1.25.0-1.25.0': futureTime
}
mockSystemStatsStore.systemStats = {
system: {
comfyui_version: '1.25.0',
@@ -242,8 +258,31 @@ describe('useVersionCompatibilityStore', () => {
expect(store.shouldShowWarning).toBe(false)
})
it('should show warning if dismissal has expired', async () => {
const pastTime = Date.now() - 1000 // Expired
mockDismissalStorage.value = {
'1.24.0-1.25.0-1.25.0': 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 (1.25.0) but current is 1.26.0
mockDismissalStorage.value = {
'1.24.0-1.25.0-1.25.0': futureTime // Different version was dismissed
}
mockSystemStatsStore.systemStats = {
system: {
comfyui_version: '1.26.0',