mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-25 16:59:45 +00:00
Feature Implemented: Warning displayed when frontend version mismatches (#4363)
Co-authored-by: bymyself <cbyrne@comfy.org> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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', () => {
|
||||
|
||||
321
tests-ui/tests/store/versionCompatibilityStore.test.ts
Normal file
321
tests-ui/tests/store/versionCompatibilityStore.test.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
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'
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
default: {
|
||||
app_version: '1.24.0'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/systemStatsStore')
|
||||
|
||||
// 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
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Clear the mock dismissal storage
|
||||
mockDismissalStorage.value = {}
|
||||
|
||||
mockSystemStatsStore = {
|
||||
systemStats: null,
|
||||
fetchSystemStats: vi.fn()
|
||||
}
|
||||
|
||||
vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore)
|
||||
|
||||
store = useVersionCompatibilityStore()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
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 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',
|
||||
required_frontend_version: '1.23.0'
|
||||
}
|
||||
}
|
||||
|
||||
await store.checkVersionCompatibility()
|
||||
|
||||
expect(store.isFrontendOutdated).toBe(false)
|
||||
expect(store.isFrontendNewer).toBe(false)
|
||||
expect(store.hasVersionMismatch).toBe(false)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
it('should not detect mismatch when versions are not valid semver', async () => {
|
||||
mockSystemStatsStore.systemStats = {
|
||||
system: {
|
||||
comfyui_version: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be', // git hash
|
||||
required_frontend_version: 'not-a-version' // invalid semver format
|
||||
}
|
||||
}
|
||||
|
||||
await store.checkVersionCompatibility()
|
||||
|
||||
expect(store.isFrontendOutdated).toBe(false)
|
||||
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', () => {
|
||||
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',
|
||||
required_frontend_version: '1.25.0'
|
||||
}
|
||||
}
|
||||
|
||||
await store.checkVersionCompatibility()
|
||||
|
||||
expect(store.shouldShowWarning).toBe(true)
|
||||
})
|
||||
|
||||
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',
|
||||
required_frontend_version: '1.25.0'
|
||||
}
|
||||
}
|
||||
|
||||
await store.checkVersionCompatibility()
|
||||
|
||||
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 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 reactive storage with expiration', async () => {
|
||||
const mockNow = 1000000
|
||||
vi.spyOn(Date, 'now').mockReturnValue(mockNow)
|
||||
|
||||
mockSystemStatsStore.systemStats = {
|
||||
system: {
|
||||
comfyui_version: '1.25.0',
|
||||
required_frontend_version: '1.25.0'
|
||||
}
|
||||
}
|
||||
|
||||
await store.checkVersionCompatibility()
|
||||
store.dismissWarning()
|
||||
|
||||
// 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 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',
|
||||
required_frontend_version: '1.25.0'
|
||||
}
|
||||
}
|
||||
|
||||
await store.initialize()
|
||||
|
||||
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 () => {
|
||||
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',
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user