[refactor] Replace manual semantic version utilities/functions with semver package (#5653)

## Summary
- Replace custom `compareVersions()` with `semver.compare()`
- Replace custom `isSemVer()` with `semver.valid()`  
- Remove deprecated version comparison functions from `formatUtil.ts`
- Update all version comparison logic across components and stores
- Fix tests to use semver mocking instead of formatUtil mocking

## Benefits
- **Industry standard**: Uses well-maintained, battle-tested `semver`
package
- **Better reliability**: Handles edge cases more robustly than custom
implementation
- **Consistent behavior**: All version comparisons now use the same
underlying logic
- **Type safety**: Better TypeScript support with proper semver types


Fixes #4787

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5653-refactor-Replace-manual-semantic-version-utilities-functions-with-semver-package-2736d73d365081fb8498ee11cbcc10e2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-09-19 12:27:49 -07:00
committed by GitHub
parent 4f5bbe0605
commit df2fda6077
12 changed files with 118 additions and 152 deletions

View File

@@ -43,11 +43,11 @@
<script setup lang="ts"> <script setup lang="ts">
import Message from 'primevue/message' import Message from 'primevue/message'
import { compare } from 'semver'
import { computed } from 'vue' import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore' import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { compareVersions } from '@/utils/formatUtil'
const props = defineProps<{ const props = defineProps<{
missingCoreNodes: Record<string, LGraphNode[]> missingCoreNodes: Record<string, LGraphNode[]>
@@ -68,7 +68,7 @@ const currentComfyUIVersion = computed<string | null>(() => {
const sortedMissingCoreNodes = computed(() => { const sortedMissingCoreNodes = computed(() => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => { return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
// Sort by version in descending order (newest first) // Sort by version in descending order (newest first)
return compareVersions(b, a) // Reversed for descending order return compare(b, a) // Reversed for descending order
}) })
}) })

View File

@@ -1,7 +1,7 @@
import { compare, valid } from 'semver'
import { computed } from 'vue' import { computed } from 'vue'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
export const usePackUpdateStatus = ( export const usePackUpdateStatus = (
@@ -16,14 +16,14 @@ export const usePackUpdateStatus = (
const latestVersion = computed(() => nodePack.latest_version?.version) const latestVersion = computed(() => nodePack.latest_version?.version)
const isNightlyPack = computed( const isNightlyPack = computed(
() => !!installedVersion.value && !isSemVer(installedVersion.value) () => !!installedVersion.value && !valid(installedVersion.value)
) )
const isUpdateAvailable = computed(() => { const isUpdateAvailable = computed(() => {
if (!isInstalled.value || isNightlyPack.value || !latestVersion.value) { if (!isInstalled.value || isNightlyPack.value || !latestVersion.value) {
return false return false
} }
return compareVersions(latestVersion.value, installedVersion.value) > 0 return compare(latestVersion.value, installedVersion.value) > 0
}) })
return { return {

View File

@@ -1,8 +1,8 @@
import { compare, valid } from 'semver'
import { computed, onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks' import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
/** /**
@@ -25,13 +25,13 @@ export const useUpdateAvailableNodes = () => {
) )
const latestVersion = pack.latest_version?.version const latestVersion = pack.latest_version?.version
const isNightlyPack = !!installedVersion && !isSemVer(installedVersion) const isNightlyPack = !!installedVersion && !valid(installedVersion)
if (isNightlyPack || !latestVersion) { if (isNightlyPack || !latestVersion) {
return false return false
} }
return compareVersions(latestVersion, installedVersion) > 0 return compare(latestVersion, installedVersion) > 0
} }
// Same filtering logic as ManagerDialogContent.vue // Same filtering logic as ManagerDialogContent.vue

View File

@@ -1,5 +1,6 @@
import _ from 'es-toolkit/compat' import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { compare, valid } from 'semver'
import { ref } from 'vue' import { ref } from 'vue'
import type { SettingParams } from '@/platform/settings/types' import type { SettingParams } from '@/platform/settings/types'
@@ -7,7 +8,6 @@ import type { Settings } from '@/schemas/apiSchema'
import { api } from '@/scripts/api' import { api } from '@/scripts/api'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import type { TreeNode } from '@/types/treeExplorerTypes' import type { TreeNode } from '@/types/treeExplorerTypes'
import { compareVersions, isSemVer } from '@/utils/formatUtil'
export const getSettingInfo = (setting: SettingParams) => { export const getSettingInfo = (setting: SettingParams) => {
const parts = setting.category || setting.id.split('.') const parts = setting.category || setting.id.split('.')
@@ -132,20 +132,25 @@ export const useSettingStore = defineStore('setting', () => {
if (installedVersion) { if (installedVersion) {
const sortedVersions = Object.keys(defaultsByInstallVersion).sort( const sortedVersions = Object.keys(defaultsByInstallVersion).sort(
(a, b) => compareVersions(b, a) (a, b) => compare(b, a)
) )
for (const version of sortedVersions) { for (const version of sortedVersions) {
// Ensure the version is in a valid format before comparing // Ensure the version is in a valid format before comparing
if (!isSemVer(version)) { if (!valid(version)) {
continue continue
} }
if (compareVersions(installedVersion, version) >= 0) { if (compare(installedVersion, version) >= 0) {
const versionedDefault = defaultsByInstallVersion[version] const versionedDefault =
return typeof versionedDefault === 'function' defaultsByInstallVersion[
? versionedDefault() version as keyof typeof defaultsByInstallVersion
: versionedDefault ]
if (versionedDefault !== undefined) {
return typeof versionedDefault === 'function'
? versionedDefault()
: versionedDefault
}
} }
} }
} }

View File

@@ -1,11 +1,12 @@
import { until } from '@vueuse/core' import { until } from '@vueuse/core'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { compare } from 'semver'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore' import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { isElectron } from '@/utils/envUtil' import { isElectron } from '@/utils/envUtil'
import { compareVersions, stringToLocale } from '@/utils/formatUtil' import { stringToLocale } from '@/utils/formatUtil'
import { type ReleaseNote, useReleaseService } from './releaseService' import { type ReleaseNote, useReleaseService } from './releaseService'
@@ -56,16 +57,19 @@ export const useReleaseStore = defineStore('release', () => {
const isNewVersionAvailable = computed( const isNewVersionAvailable = computed(
() => () =>
!!recentRelease.value && !!recentRelease.value &&
compareVersions( compare(
recentRelease.value.version, recentRelease.value.version,
currentComfyUIVersion.value currentComfyUIVersion.value || '0.0.0'
) > 0 ) > 0
) )
const isLatestVersion = computed( const isLatestVersion = computed(
() => () =>
!!recentRelease.value && !!recentRelease.value &&
!compareVersions(recentRelease.value.version, currentComfyUIVersion.value) compare(
recentRelease.value.version,
currentComfyUIVersion.value || '0.0.0'
) === 0
) )
const hasMediumOrHighAttention = computed(() => const hasMediumOrHighAttention = computed(() =>

View File

@@ -1,6 +1,6 @@
import { until, useStorage } from '@vueuse/core' import { until, useStorage } from '@vueuse/core'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import * as semver from 'semver' import { gt, valid } from 'semver'
import { computed } from 'vue' import { computed } from 'vue'
import config from '@/config' import config from '@/config'
@@ -26,13 +26,13 @@ export const useVersionCompatibilityStore = defineStore(
if ( if (
!frontendVersion.value || !frontendVersion.value ||
!requiredFrontendVersion.value || !requiredFrontendVersion.value ||
!semver.valid(frontendVersion.value) || !valid(frontendVersion.value) ||
!semver.valid(requiredFrontendVersion.value) !valid(requiredFrontendVersion.value)
) { ) {
return false return false
} }
// Returns true if required version is greater than frontend version // Returns true if required version is greater than frontend version
return semver.gt(requiredFrontendVersion.value, frontendVersion.value) return gt(requiredFrontendVersion.value, frontendVersion.value)
}) })
const isFrontendNewer = computed(() => { const isFrontendNewer = computed(() => {

View File

@@ -364,39 +364,6 @@ export const downloadUrlToHfRepoUrl = (url: string): string => {
} }
} }
export const isSemVer = (
version: string
): version is `${number}.${number}.${number}` => {
const regex = /^\d+\.\d+\.\d+$/
return regex.test(version)
}
const normalizeVersion = (version: string) =>
version
.split(/[+.-]/)
.map(Number)
.filter((part) => !Number.isNaN(part))
export function compareVersions(
versionA: string | undefined,
versionB: string | undefined
): number {
versionA ??= '0.0.0'
versionB ??= '0.0.0'
const aParts = normalizeVersion(versionA)
const bParts = normalizeVersion(versionB)
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
const aPart = aParts[i] ?? 0
const bPart = bParts[i] ?? 0
if (aPart < bPart) return -1
if (aPart > bPart) return 1
}
return 0
}
/** /**
* Converts Metronome's integer amount back to a formatted currency string. * Converts Metronome's integer amount back to a formatted currency string.
* For USD, converts from cents to dollars. * For USD, converts from cents to dollars.

View File

@@ -1,4 +1,4 @@
import * as semver from 'semver' import { clean, satisfies } from 'semver'
import type { import type {
ConflictDetail, ConflictDetail,
@@ -11,7 +11,7 @@ import type {
* @returns Cleaned version string or original if cleaning fails * @returns Cleaned version string or original if cleaning fails
*/ */
export function cleanVersion(version: string): string { export function cleanVersion(version: string): string {
return semver.clean(version) || version return clean(version) || version
} }
/** /**
@@ -23,7 +23,7 @@ export function cleanVersion(version: string): string {
export function satisfiesVersion(version: string, range: string): boolean { export function satisfiesVersion(version: string, range: string): boolean {
try { try {
const cleanedVersion = cleanVersion(version) const cleanedVersion = cleanVersion(version)
return semver.satisfies(cleanedVersion, range) return satisfies(cleanedVersion, range)
} catch { } catch {
return false return false
} }

View File

@@ -43,11 +43,11 @@
<script setup lang="ts"> <script setup lang="ts">
import Popover from 'primevue/popover' import Popover from 'primevue/popover'
import { valid as validSemver } from 'semver'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus' import { usePackUpdateStatus } from '@/composables/nodePack/usePackUpdateStatus'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import { isSemVer } from '@/utils/formatUtil'
import PackVersionSelectorPopover from '@/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue' import PackVersionSelectorPopover from '@/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
@@ -81,7 +81,9 @@ const installedVersion = computed(() => {
'nightly' 'nightly'
// If Git hash, truncate to 7 characters // If Git hash, truncate to 7 characters
return isSemVer(version) ? version : version.slice(0, TRUNCATED_HASH_LENGTH) return validSemver(version)
? version
: version.slice(0, TRUNCATED_HASH_LENGTH)
}) })
const toggleVersionSelector = (event: Event) => { const toggleVersionSelector = (event: Event) => {

View File

@@ -84,6 +84,7 @@ import { whenever } from '@vueuse/core'
import Button from 'primevue/button' import Button from 'primevue/button'
import Listbox from 'primevue/listbox' import Listbox from 'primevue/listbox'
import ProgressSpinner from 'primevue/progressspinner' import ProgressSpinner from 'primevue/progressspinner'
import { valid as validSemver } from 'semver'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -94,7 +95,6 @@ import { useConflictDetection } from '@/composables/useConflictDetection'
import { useComfyRegistryService } from '@/services/comfyRegistryService' import { useComfyRegistryService } from '@/services/comfyRegistryService'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil' import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
import { isSemVer } from '@/utils/formatUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes' import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
@@ -142,7 +142,7 @@ onMounted(() => {
getInitialSelectedVersion() ?? SelectedVersionValues.LATEST getInitialSelectedVersion() ?? SelectedVersionValues.LATEST
selectedVersion.value = selectedVersion.value =
// Use NIGHTLY when version is a Git hash // Use NIGHTLY when version is a Git hash
isSemVer(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY validSemver(initialVersion) ? initialVersion : SelectedVersionValues.NIGHTLY
}) })
const getInitialSelectedVersion = () => { const getInitialSelectedVersion = () => {

View File

@@ -1,10 +1,9 @@
import { compare, valid } from 'semver'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue' import { nextTick, ref } from 'vue'
import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks' import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks'
import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes' import { useUpdateAvailableNodes } from '@/composables/nodePack/useUpdateAvailableNodes'
// Import mocked utils
import { compareVersions, isSemVer } from '@/utils/formatUtil'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
// Mock Vue's onMounted to execute immediately for testing // Mock Vue's onMounted to execute immediately for testing
@@ -25,16 +24,16 @@ vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
useComfyManagerStore: vi.fn() useComfyManagerStore: vi.fn()
})) }))
vi.mock('@/utils/formatUtil', () => ({ vi.mock('semver', () => ({
compareVersions: vi.fn(), compare: vi.fn(),
isSemVer: vi.fn() valid: vi.fn()
})) }))
const mockUseInstalledPacks = vi.mocked(useInstalledPacks) const mockUseInstalledPacks = vi.mocked(useInstalledPacks)
const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore) const mockUseComfyManagerStore = vi.mocked(useComfyManagerStore)
const mockCompareVersions = vi.mocked(compareVersions) const mockSemverCompare = vi.mocked(compare)
const mockIsSemVer = vi.mocked(isSemVer) const mockSemverValid = vi.mocked(valid)
describe('useUpdateAvailableNodes', () => { describe('useUpdateAvailableNodes', () => {
const mockInstalledPacks = [ const mockInstalledPacks = [
@@ -86,19 +85,19 @@ describe('useUpdateAvailableNodes', () => {
} }
}) })
mockIsSemVer.mockImplementation( mockSemverValid.mockImplementation((version) => {
(version: string): version is `${number}.${number}.${number}` => { return version &&
return !version.includes('nightly') typeof version === 'string' &&
} !version.includes('nightly')
) ? version
: null
})
mockCompareVersions.mockImplementation( mockSemverCompare.mockImplementation((latest, installed) => {
(latest: string | undefined, installed: string | undefined) => { if (latest === '2.0.0' && installed === '1.0.0') return 1 // outdated
if (latest === '2.0.0' && installed === '1.0.0') return 1 // outdated if (latest === '1.0.0' && installed === '1.0.0') return 0 // up to date
if (latest === '1.0.0' && installed === '1.0.0') return 0 // up to date return 0
return 0 })
}
)
mockUseComfyManagerStore.mockReturnValue({ mockUseComfyManagerStore.mockReturnValue({
isPackInstalled: mockIsPackInstalled, isPackInstalled: mockIsPackInstalled,
@@ -322,10 +321,10 @@ describe('useUpdateAvailableNodes', () => {
// Access the computed to trigger the logic // Access the computed to trigger the logic
expect(updateAvailableNodePacks.value).toBeDefined() expect(updateAvailableNodePacks.value).toBeDefined()
expect(mockCompareVersions).toHaveBeenCalledWith('2.0.0', '1.0.0') expect(mockSemverCompare).toHaveBeenCalledWith('2.0.0', '1.0.0')
}) })
it('calls isSemVer to check nightly versions', () => { it('calls semver.valid to check nightly versions', () => {
mockUseInstalledPacks.mockReturnValue({ mockUseInstalledPacks.mockReturnValue({
installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly installedPacks: ref([mockInstalledPacks[2]]), // pack-3: nightly
isLoading: ref(false), isLoading: ref(false),
@@ -338,7 +337,7 @@ describe('useUpdateAvailableNodes', () => {
// Access the computed to trigger the logic // Access the computed to trigger the logic
expect(updateAvailableNodePacks.value).toBeDefined() expect(updateAvailableNodePacks.value).toBeDefined()
expect(mockIsSemVer).toHaveBeenCalledWith('nightly-abc123') expect(mockSemverValid).toHaveBeenCalledWith('nightly-abc123')
}) })
it('calls isPackInstalled for each pack', () => { it('calls isPackInstalled for each pack', () => {

View File

@@ -1,10 +1,11 @@
import { createPinia, setActivePinia } from 'pinia' import { createPinia, setActivePinia } from 'pinia'
import { compare as semverCompare } from 'semver'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useReleaseStore } from '@/platform/updates/common/releaseStore' import { useReleaseStore } from '@/platform/updates/common/releaseStore'
// Mock the dependencies // Mock the dependencies
vi.mock('@/utils/formatUtil') vi.mock('semver')
vi.mock('@/utils/envUtil') vi.mock('@/utils/envUtil')
vi.mock('@/platform/updates/common/releaseService') vi.mock('@/platform/updates/common/releaseService')
vi.mock('@/platform/settings/settingStore') vi.mock('@/platform/settings/settingStore')
@@ -113,17 +114,15 @@ describe('useReleaseStore', () => {
expect(store.recentReleases).toEqual(releases.slice(0, 3)) expect(store.recentReleases).toEqual(releases.slice(0, 3))
}) })
it('should show update button (shouldShowUpdateButton)', async () => { it('should show update button (shouldShowUpdateButton)', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1) // newer version available
vi.mocked(compareVersions).mockReturnValue(1) // newer version available
store.releases = [mockRelease] store.releases = [mockRelease]
expect(store.shouldShowUpdateButton).toBe(true) expect(store.shouldShowUpdateButton).toBe(true)
}) })
it('should not show update button when no new version', async () => { it('should not show update button when no new version', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(-1) // current version is newer
vi.mocked(compareVersions).mockReturnValue(-1) // current version is newer
store.releases = [mockRelease] store.releases = [mockRelease]
expect(store.shouldShowUpdateButton).toBe(false) expect(store.shouldShowUpdateButton).toBe(false)
@@ -131,21 +130,20 @@ describe('useReleaseStore', () => {
}) })
describe('showVersionUpdates setting', () => { describe('showVersionUpdates setting', () => {
beforeEach(() => { beforeEach(async () => {
store.releases = [mockRelease] store.releases = [mockRelease]
}) })
describe('when notifications are enabled', () => { describe('when notifications are enabled', () => {
beforeEach(() => { beforeEach(async () => {
mockSettingStore.get.mockImplementation((key: string) => { mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true if (key === 'Comfy.Notification.ShowVersionUpdates') return true
return null return null
}) })
}) })
it('should show toast for medium/high attention releases', async () => { it('should show toast for medium/high attention releases', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
// Need multiple releases for hasMediumOrHighAttention to work // Need multiple releases for hasMediumOrHighAttention to work
const mediumRelease = { const mediumRelease = {
@@ -158,17 +156,16 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(true) expect(store.shouldShowToast).toBe(true)
}) })
it('should show red dot for new versions', async () => { it('should show red dot for new versions', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(true) expect(store.shouldShowRedDot).toBe(true)
}) })
it('should show popup for latest version', async () => { it('should show popup for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0) vi.mocked(semverCompare).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(true) expect(store.shouldShowPopup).toBe(true)
}) })
@@ -181,37 +178,36 @@ describe('useReleaseStore', () => {
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({ expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui', project: 'comfyui',
current_version: '1.0.0', current_version: '1.0.0',
form_factor: 'git-windows' form_factor: 'git-windows',
locale: 'en'
}) })
}) })
}) })
describe('when notifications are disabled', () => { describe('when notifications are disabled', () => {
beforeEach(() => { beforeEach(async () => {
mockSettingStore.get.mockImplementation((key: string) => { mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return false if (key === 'Comfy.Notification.ShowVersionUpdates') return false
return null return null
}) })
}) })
it('should not show toast even with new version available', async () => { it('should not show toast even with new version available', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowToast).toBe(false) expect(store.shouldShowToast).toBe(false)
}) })
it('should not show red dot even with new version available', async () => { it('should not show red dot even with new version available', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(false) expect(store.shouldShowRedDot).toBe(false)
}) })
it('should not show popup even for latest version', async () => { it('should not show popup even for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0) vi.mocked(semverCompare).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(false) expect(store.shouldShowPopup).toBe(false)
}) })
@@ -240,7 +236,8 @@ describe('useReleaseStore', () => {
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({ expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui', project: 'comfyui',
current_version: '1.0.0', current_version: '1.0.0',
form_factor: 'git-windows' form_factor: 'git-windows',
locale: 'en'
}) })
expect(store.releases).toEqual([mockRelease]) expect(store.releases).toEqual([mockRelease])
}) })
@@ -254,7 +251,8 @@ describe('useReleaseStore', () => {
expect(mockReleaseService.getReleases).toHaveBeenCalledWith({ expect(mockReleaseService.getReleases).toHaveBeenCalledWith({
project: 'comfyui', project: 'comfyui',
current_version: '1.0.0', current_version: '1.0.0',
form_factor: 'desktop-mac' form_factor: 'desktop-mac',
locale: 'en'
}) })
}) })
@@ -424,7 +422,7 @@ describe('useReleaseStore', () => {
}) })
describe('action handlers', () => { describe('action handlers', () => {
beforeEach(() => { beforeEach(async () => {
store.releases = [mockRelease] store.releases = [mockRelease]
}) })
@@ -481,7 +479,7 @@ describe('useReleaseStore', () => {
}) })
describe('popup visibility', () => { describe('popup visibility', () => {
it('should show toast for medium/high attention releases', async () => { it('should show toast for medium/high attention releases', () => {
mockSettingStore.get.mockImplementation((key: string) => { mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Release.Version') return null if (key === 'Comfy.Release.Version') return null
if (key === 'Comfy.Release.Status') return null if (key === 'Comfy.Release.Status') return null
@@ -489,8 +487,7 @@ describe('useReleaseStore', () => {
return null return null
}) })
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
const mediumRelease = { ...mockRelease, attention: 'medium' as const } const mediumRelease = { ...mockRelease, attention: 'medium' as const }
store.releases = [ store.releases = [
@@ -502,9 +499,8 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(true) expect(store.shouldShowToast).toBe(true)
}) })
it('should show red dot for new versions', async () => { it('should show red dot for new versions', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
mockSettingStore.get.mockImplementation((key: string) => { mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true if (key === 'Comfy.Notification.ShowVersionUpdates') return true
return null return null
@@ -515,15 +511,14 @@ describe('useReleaseStore', () => {
expect(store.shouldShowRedDot).toBe(true) expect(store.shouldShowRedDot).toBe(true)
}) })
it('should show popup for latest version', async () => { it('should show popup for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' // Same as release mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' // Same as release
mockSettingStore.get.mockImplementation((key: string) => { mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true if (key === 'Comfy.Notification.ShowVersionUpdates') return true
return null return null
}) })
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(0) // versions are equal (latest version)
vi.mocked(compareVersions).mockReturnValue(0) // versions are equal (latest version)
store.releases = [mockRelease] store.releases = [mockRelease]
@@ -565,7 +560,7 @@ describe('useReleaseStore', () => {
}) })
describe('isElectron environment checks', () => { describe('isElectron environment checks', () => {
beforeEach(() => { beforeEach(async () => {
// Set up a new version available // Set up a new version available
store.releases = [mockRelease] store.releases = [mockRelease]
mockSettingStore.get.mockImplementation((key: string) => { mockSettingStore.get.mockImplementation((key: string) => {
@@ -580,9 +575,8 @@ describe('useReleaseStore', () => {
vi.mocked(isElectron).mockReturnValue(true) vi.mocked(isElectron).mockReturnValue(true)
}) })
it('should show toast when conditions are met', async () => { it('should show toast when conditions are met', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
// Need multiple releases for hasMediumOrHighAttention // Need multiple releases for hasMediumOrHighAttention
const mediumRelease = { const mediumRelease = {
@@ -595,17 +589,16 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(true) expect(store.shouldShowToast).toBe(true)
}) })
it('should show red dot when new version available', async () => { it('should show red dot when new version available', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(true) expect(store.shouldShowRedDot).toBe(true)
}) })
it('should show popup for latest version', async () => { it('should show popup for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0) vi.mocked(semverCompare).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(true) expect(store.shouldShowPopup).toBe(true)
}) })
@@ -617,9 +610,8 @@ describe('useReleaseStore', () => {
vi.mocked(isElectron).mockReturnValue(false) vi.mocked(isElectron).mockReturnValue(false)
}) })
it('should NOT show toast even when all other conditions are met', async () => { it('should NOT show toast even when all other conditions are met', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
// Set up all conditions that would normally show toast // Set up all conditions that would normally show toast
const mediumRelease = { const mediumRelease = {
@@ -632,16 +624,14 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(false) expect(store.shouldShowToast).toBe(false)
}) })
it('should NOT show red dot even when new version available', async () => { it('should NOT show red dot even when new version available', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
expect(store.shouldShowRedDot).toBe(false) expect(store.shouldShowRedDot).toBe(false)
}) })
it('should NOT show toast regardless of attention level', async () => { it('should NOT show toast regardless of attention level', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
// Test with high attention releases // Test with high attention releases
const highRelease = { const highRelease = {
@@ -659,19 +649,18 @@ describe('useReleaseStore', () => {
expect(store.shouldShowToast).toBe(false) expect(store.shouldShowToast).toBe(false)
}) })
it('should NOT show red dot even with high attention release', async () => { it('should NOT show red dot even with high attention release', () => {
const { compareVersions } = await import('@/utils/formatUtil') vi.mocked(semverCompare).mockReturnValue(1)
vi.mocked(compareVersions).mockReturnValue(1)
store.releases = [{ ...mockRelease, attention: 'high' as const }] store.releases = [{ ...mockRelease, attention: 'high' as const }]
expect(store.shouldShowRedDot).toBe(false) expect(store.shouldShowRedDot).toBe(false)
}) })
it('should NOT show popup even for latest version', async () => { it('should NOT show popup even for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
const { compareVersions } = await import('@/utils/formatUtil')
vi.mocked(compareVersions).mockReturnValue(0) vi.mocked(semverCompare).mockReturnValue(0)
expect(store.shouldShowPopup).toBe(false) expect(store.shouldShowPopup).toBe(false)
}) })