mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-16 16:58:33 +00:00
Compare commits
5 Commits
codex/revi
...
feature/sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af99419723 | ||
|
|
39b66c06d0 | ||
|
|
57c74f46c9 | ||
|
|
df7eaab505 | ||
|
|
9198f13636 |
@@ -138,6 +138,10 @@ test.describe('Help Center', () => {
|
||||
)
|
||||
|
||||
await helpCenter.mockReleases(releases)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Notification.ShowVersionUpdates',
|
||||
true
|
||||
)
|
||||
await comfyPage.setup({ mockReleases: false })
|
||||
await helpCenter.open()
|
||||
|
||||
@@ -157,6 +161,10 @@ test.describe('Help Center', () => {
|
||||
|
||||
await helpCenter.mockReleases([release])
|
||||
await helpCenter.stubDocsPage()
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Notification.ShowVersionUpdates',
|
||||
true
|
||||
)
|
||||
await comfyPage.setup({ mockReleases: false })
|
||||
await helpCenter.open()
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ test.describe('Release Notifications', () => {
|
||||
test('should show help center with release information', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Version-update notifications default off on local installs
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Notification.ShowVersionUpdates',
|
||||
true
|
||||
)
|
||||
|
||||
// Mock release API with test data instead of empty array
|
||||
await comfyPage.page.route('**/releases**', async (route) => {
|
||||
const url = route.request().url()
|
||||
@@ -72,10 +78,10 @@ test.describe('Release Notifications', () => {
|
||||
await expect(helpMenu).toBeHidden()
|
||||
})
|
||||
|
||||
test('should not show release notifications when mocked (default behavior)', async ({
|
||||
test('should hide "What\'s New" section by default on local installs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Use default setup (mockReleases: true)
|
||||
// Use default setup (mockReleases: true); notifications default off locally
|
||||
await comfyPage.setup()
|
||||
|
||||
// Open help center
|
||||
@@ -87,16 +93,11 @@ test.describe('Release Notifications', () => {
|
||||
const helpMenu = comfyPage.page.locator('.help-center-menu')
|
||||
await expect(helpMenu).toBeVisible()
|
||||
|
||||
// Verify "What's New?" section shows no releases
|
||||
// "What's New?" section is hidden because the setting defaults off
|
||||
const whatsNewSection = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.whatsNewSection
|
||||
)
|
||||
await expect(whatsNewSection).toBeVisible()
|
||||
|
||||
// Should show "No recent releases" message
|
||||
await expect(
|
||||
whatsNewSection.locator('text=No recent releases')
|
||||
).toBeVisible()
|
||||
await expect(whatsNewSection).toBeHidden()
|
||||
|
||||
// Should not show any popups or toasts
|
||||
await expect(comfyPage.page.locator('.whats-new-popup')).toBeHidden()
|
||||
@@ -106,6 +107,12 @@ test.describe('Release Notifications', () => {
|
||||
})
|
||||
|
||||
test('should handle release API errors gracefully', async ({ comfyPage }) => {
|
||||
// Version-update notifications default off on local installs
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Notification.ShowVersionUpdates',
|
||||
true
|
||||
)
|
||||
|
||||
// Mock API to return an error
|
||||
await comfyPage.page.route('**/releases**', async (route) => {
|
||||
const url = route.request().url()
|
||||
|
||||
@@ -33,7 +33,8 @@ export enum ServerFeatureFlag {
|
||||
SHOW_SIGNIN_BUTTON = 'show_signin_button',
|
||||
UNIFIED_CLOUD_AUTH = 'unified_cloud_auth',
|
||||
CONSOLIDATED_BILLING_ENABLED = 'consolidated_billing_enabled',
|
||||
SIGNUP_TURNSTILE = 'signup_turnstile'
|
||||
SIGNUP_TURNSTILE = 'signup_turnstile',
|
||||
SHOW_VERSION_UPDATES = 'show_version_updates'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
55
src/platform/settings/constants/coreSettings.test.ts
Normal file
55
src/platform/settings/constants/coreSettings.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import { CORE_SETTINGS } from './coreSettings'
|
||||
|
||||
const mockDist = vi.hoisted(() => ({ isCloud: false }))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isDesktop: false,
|
||||
isNightly: false,
|
||||
get isCloud() {
|
||||
return mockDist.isCloud
|
||||
}
|
||||
}))
|
||||
|
||||
function getDefault(id: string) {
|
||||
const setting = CORE_SETTINGS.find((s) => s.id === id)
|
||||
if (!setting) throw new Error(`Setting ${id} not found`)
|
||||
const { defaultValue } = setting
|
||||
return typeof defaultValue === 'function' ? defaultValue() : defaultValue
|
||||
}
|
||||
|
||||
describe('Comfy.Notification.ShowVersionUpdates default', () => {
|
||||
beforeEach(() => {
|
||||
mockDist.isCloud = false
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('resolves to the show_version_updates server flag value', () => {
|
||||
vi.spyOn(api, 'getServerFeature').mockReturnValue(true)
|
||||
expect(getDefault('Comfy.Notification.ShowVersionUpdates')).toBe(true)
|
||||
|
||||
vi.spyOn(api, 'getServerFeature').mockReturnValue(false)
|
||||
expect(getDefault('Comfy.Notification.ShowVersionUpdates')).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to off on local installs when the server flag is unset', () => {
|
||||
vi.spyOn(api, 'getServerFeature').mockImplementation(
|
||||
(_name, fallback) => fallback
|
||||
)
|
||||
expect(getDefault('Comfy.Notification.ShowVersionUpdates')).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to on for Cloud when the server flag is unset', () => {
|
||||
mockDist.isCloud = true
|
||||
vi.spyOn(api, 'getServerFeature').mockImplementation(
|
||||
(_name, fallback) => fallback
|
||||
)
|
||||
expect(getDefault('Comfy.Notification.ShowVersionUpdates')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
import { isCloud, isDesktop, isNightly } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
import { ServerFeatureFlag } from '@/composables/useFeatureFlags'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ColorPalettes } from '@/schemas/colorPaletteSchema'
|
||||
import type { Keybinding } from '@/platform/keybindings/types'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
@@ -484,7 +486,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
name: 'Show version updates',
|
||||
tooltip: 'Show updates for new models, and major new features.',
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
defaultValue: () =>
|
||||
api.getServerFeature(ServerFeatureFlag.SHOW_VERSION_UPDATES, isCloud)
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ConfirmClear',
|
||||
|
||||
118
src/platform/updates/common/releaseStore.initialize.test.ts
Normal file
118
src/platform/updates/common/releaseStore.initialize.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { useReleaseService } from '@/platform/updates/common/releaseService'
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import type { SystemStats } from '@/types'
|
||||
|
||||
// Unlike releaseStore.test.ts, this file does NOT mock '@vueuse/core', so the
|
||||
// real `until` runs and the server-feature-flag gate in initialize() is
|
||||
// genuinely exercised (regression coverage for the default-off fix).
|
||||
|
||||
const mockData = vi.hoisted(() => ({ isCloud: false }))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isDesktop: false,
|
||||
get isCloud() {
|
||||
return mockData.isCloud
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/updates/common/releaseService', async () => {
|
||||
const { ref } = await import('vue')
|
||||
const getReleases = vi.fn()
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
return {
|
||||
useReleaseService: () => ({ getReleases, isLoading, error })
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/scripts/api', async () => {
|
||||
const { ref } = await import('vue')
|
||||
return {
|
||||
api: { serverFeatureFlags: ref<Record<string, unknown>>({}) }
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => {
|
||||
const get = vi.fn((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
return {
|
||||
useSettingStore: () => ({ get, set: vi.fn(), setMany: vi.fn() })
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/stores/systemStatsStore', () => {
|
||||
const systemStats: { system: Partial<SystemStats['system']> } = {
|
||||
system: { comfyui_version: '1.0.0', argv: [] }
|
||||
}
|
||||
return {
|
||||
useSystemStatsStore: () => ({
|
||||
systemStats,
|
||||
isInitialized: true,
|
||||
getFormFactor: vi.fn(() => 'git-windows')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('useReleaseStore initialize gating', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
api.serverFeatureFlags.value = {}
|
||||
mockData.isCloud = false
|
||||
vi.mocked(useReleaseService().getReleases).mockResolvedValue([])
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('waits for server feature flags before fetching releases', async () => {
|
||||
const store = useReleaseStore()
|
||||
const releaseService = useReleaseService()
|
||||
|
||||
const initializePromise = store.initialize()
|
||||
await nextTick()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(releaseService.getReleases).not.toHaveBeenCalled()
|
||||
|
||||
api.serverFeatureFlags.value = { show_version_updates: true }
|
||||
await initializePromise
|
||||
|
||||
expect(releaseService.getReleases).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('falls back to fetching after the timeout when flags never arrive', async () => {
|
||||
vi.useFakeTimers()
|
||||
const store = useReleaseStore()
|
||||
const releaseService = useReleaseService()
|
||||
|
||||
const initializePromise = store.initialize()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(releaseService.getReleases).not.toHaveBeenCalled()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(3000)
|
||||
await initializePromise
|
||||
|
||||
expect(releaseService.getReleases).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not gate on feature flags for cloud installs', async () => {
|
||||
mockData.isCloud = true
|
||||
const store = useReleaseStore()
|
||||
const releaseService = useReleaseService()
|
||||
|
||||
await store.initialize()
|
||||
|
||||
expect(releaseService.getReleases).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -18,13 +18,13 @@ vi.mock('semver', () => ({
|
||||
valid: vi.fn(() => '1.0.0')
|
||||
}))
|
||||
|
||||
const mockData = vi.hoisted(() => ({ isDesktop: true }))
|
||||
const mockData = vi.hoisted(() => ({ isCloud: false }))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
get isDesktop() {
|
||||
return mockData.isDesktop
|
||||
},
|
||||
isCloud: false
|
||||
isDesktop: false,
|
||||
get isCloud() {
|
||||
return mockData.isCloud
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/updates/common/releaseService', () => {
|
||||
@@ -40,6 +40,14 @@ vi.mock('@/platform/updates/common/releaseService', () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
// Non-empty => feature flags already received, so fetchReleases' gate
|
||||
// resolves immediately instead of waiting on the websocket.
|
||||
serverFeatureFlags: ref<Record<string, unknown>>({ ready: true })
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => {
|
||||
const get = vi.fn((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
@@ -95,7 +103,12 @@ vi.mock('@/stores/systemStatsStore', () => {
|
||||
}
|
||||
})
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
until: vi.fn(() => Promise.resolve()),
|
||||
// until() is awaited directly in some call sites and chained as
|
||||
// until(...).toBe(...) in others; support both shapes.
|
||||
until: vi.fn(() => {
|
||||
const resolved = Promise.resolve()
|
||||
return Object.assign(resolved, { toBe: vi.fn(() => Promise.resolve()) })
|
||||
}),
|
||||
useStorage: vi.fn(() => ({ value: {} })),
|
||||
createSharedComposable: vi.fn((fn) => fn)
|
||||
}))
|
||||
@@ -115,6 +128,7 @@ describe('useReleaseStore', () => {
|
||||
|
||||
vi.resetAllMocks()
|
||||
mockSystemStatsState.reset()
|
||||
mockData.isCloud = false
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
@@ -442,11 +456,11 @@ describe('useReleaseStore', () => {
|
||||
|
||||
vi.mocked(releaseService.getReleases).mockReturnValue(promise)
|
||||
|
||||
const initPromise = store.initialize()
|
||||
const fetchPromise = store.fetchReleases()
|
||||
expect(store.isLoading).toBe(true)
|
||||
|
||||
resolvePromise!([mockRelease])
|
||||
await initPromise
|
||||
await fetchPromise
|
||||
|
||||
expect(store.isLoading).toBe(false)
|
||||
})
|
||||
@@ -681,102 +695,40 @@ describe('useReleaseStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('isDesktop environment checks', () => {
|
||||
describe('when running on desktop', () => {
|
||||
beforeEach(() => {
|
||||
mockData.isDesktop = true
|
||||
})
|
||||
|
||||
it('should show toast when conditions are met', () => {
|
||||
const store = useReleaseStore()
|
||||
store.releases = [mockRelease]
|
||||
vi.mocked(compare).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowToast).toBe(true)
|
||||
})
|
||||
|
||||
it('should show red dot when new version available', () => {
|
||||
const store = useReleaseStore()
|
||||
store.releases = [mockRelease]
|
||||
vi.mocked(compare).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(true)
|
||||
})
|
||||
|
||||
it('should show popup for latest version', () => {
|
||||
const store = useReleaseStore()
|
||||
store.releases = [mockRelease]
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
systemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
|
||||
|
||||
vi.mocked(compare).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(true)
|
||||
describe('cloud distribution', () => {
|
||||
beforeEach(() => {
|
||||
mockData.isCloud = true
|
||||
const settingStore = useSettingStore()
|
||||
vi.mocked(settingStore.get).mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
describe('when NOT running on desktop (web)', () => {
|
||||
beforeEach(() => {
|
||||
mockData.isDesktop = false
|
||||
})
|
||||
it('suppresses the toast even when a new version is available', () => {
|
||||
const store = useReleaseStore()
|
||||
store.releases = [mockRelease]
|
||||
vi.mocked(compare).mockReturnValue(1)
|
||||
|
||||
it('should NOT show toast even when all other conditions are met', () => {
|
||||
const store = useReleaseStore()
|
||||
vi.mocked(compare).mockReturnValue(1)
|
||||
expect(store.shouldShowToast).toBe(false)
|
||||
})
|
||||
|
||||
// Set up all conditions that would normally show toast
|
||||
store.releases = [mockRelease]
|
||||
it('suppresses the red dot even when a new version is available', () => {
|
||||
const store = useReleaseStore()
|
||||
store.releases = [mockRelease]
|
||||
vi.mocked(compare).mockReturnValue(1)
|
||||
|
||||
expect(store.shouldShowToast).toBe(false)
|
||||
})
|
||||
expect(store.shouldShowRedDot).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT show red dot even when new version available', () => {
|
||||
const store = useReleaseStore()
|
||||
store.releases = [mockRelease]
|
||||
vi.mocked(compare).mockReturnValue(1)
|
||||
it("still shows the what's-new popup for the latest version", () => {
|
||||
const store = useReleaseStore()
|
||||
store.releases = [mockRelease]
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
systemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
|
||||
vi.mocked(compare).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT show toast regardless of attention level', () => {
|
||||
const store = useReleaseStore()
|
||||
vi.mocked(compare).mockReturnValue(1)
|
||||
|
||||
// Test with high attention releases
|
||||
const highRelease = {
|
||||
...mockRelease,
|
||||
id: 2,
|
||||
attention: 'high' as const
|
||||
}
|
||||
const mediumRelease = {
|
||||
...mockRelease,
|
||||
id: 3,
|
||||
attention: 'medium' as const
|
||||
}
|
||||
store.releases = [highRelease, mediumRelease]
|
||||
|
||||
expect(store.shouldShowToast).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT show red dot even with high attention release', () => {
|
||||
const store = useReleaseStore()
|
||||
vi.mocked(compare).mockReturnValue(1)
|
||||
|
||||
store.releases = [{ ...mockRelease, attention: 'high' as const }]
|
||||
|
||||
expect(store.shouldShowRedDot).toBe(false)
|
||||
})
|
||||
|
||||
it('should NOT show popup even for latest version', () => {
|
||||
const store = useReleaseStore()
|
||||
store.releases = [mockRelease]
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
systemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
|
||||
|
||||
vi.mocked(compare).mockReturnValue(0)
|
||||
|
||||
expect(store.shouldShowPopup).toBe(false)
|
||||
})
|
||||
expect(store.shouldShowPopup).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,8 +3,9 @@ import { defineStore } from 'pinia'
|
||||
import { compare, valid } from 'semver'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { isCloud, isDesktop } from '@/platform/distribution/types'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { stringToLocale } from '@/utils/formatUtil'
|
||||
|
||||
@@ -93,8 +94,8 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
|
||||
// Show toast if needed
|
||||
const shouldShowToast = computed(() => {
|
||||
// Only show on desktop version
|
||||
if (!isDesktop || isCloud) {
|
||||
// Cloud has its own update surface; the toast is for local installs.
|
||||
if (isCloud) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -125,8 +126,8 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
|
||||
// Show red-dot indicator
|
||||
const shouldShowRedDot = computed(() => {
|
||||
// Only show on desktop version
|
||||
if (!isDesktop || isCloud) {
|
||||
// Cloud has its own update surface; the red dot is for local installs.
|
||||
if (isCloud) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -171,10 +172,8 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
})
|
||||
|
||||
const shouldShowPopup = computed(() => {
|
||||
if (!isDesktop && !isCloud) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Gated purely by the setting: off by default on local installs,
|
||||
// on by default on Cloud (seeded by the server feature flag fallback).
|
||||
if (!showVersionUpdates.value) {
|
||||
return false
|
||||
}
|
||||
@@ -294,6 +293,16 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
|
||||
// Initialize store
|
||||
async function initialize(): Promise<void> {
|
||||
// showVersionUpdates' default is seeded by the show_version_updates server
|
||||
// feature flag, which arrives over the websocket shortly after connect. Wait
|
||||
// for the flags (non-empty once received) so a host that disabled updates
|
||||
// isn't briefly treated as enabled on first load; fall back after a timeout
|
||||
// so a missing/late flag message can't block fetching forever.
|
||||
if (!isCloud) {
|
||||
await until(
|
||||
() => Object.keys(api.serverFeatureFlags.value).length > 0
|
||||
).toBe(true, { timeout: 3000 })
|
||||
}
|
||||
await fetchReleases()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user