diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts index 98cdc9e37e..e8994e4f02 100644 --- a/browser_tests/tests/graphCanvasMenu.spec.ts +++ b/browser_tests/tests/graphCanvasMenu.spec.ts @@ -9,8 +9,7 @@ test.describe('Graph Canvas Menu', () => { await comfyPage.setSetting('Comfy.LinkRenderMode', 2) }) - test.skip('Can toggle link visibility', async ({ comfyPage }) => { - // Skipped for 1.24.x: Screenshot includes minimap button which has different visual state + test('Can toggle link visibility', async ({ comfyPage }) => { // Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup. // so no cleanup is needed. await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index f2addd5a00..0b1a7636bd 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -767,7 +767,6 @@ test.describe('Viewport settings', () => { comfyPage, comfyMouse }) => { - // Skipped for 1.24.x: Minimap is disabled by default in this branch // Screenshot the canvas element await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 36aaff5a79..a7bdcd3097 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -919,6 +919,33 @@ const apiNodeCosts: Record = return `$${price.toFixed(2)}/Run` } }, + Veo3VideoGenerationNode: { + displayPrice: (node: LGraphNode): string => { + const modelWidget = node.widgets?.find( + (w) => w.name === 'model' + ) as IComboWidget + const generateAudioWidget = node.widgets?.find( + (w) => w.name === 'generate_audio' + ) as IComboWidget + + if (!modelWidget || !generateAudioWidget) { + return '$2.00-6.00/Run (varies with model & audio generation)' + } + + const model = String(modelWidget.value) + const generateAudio = + String(generateAudioWidget.value).toLowerCase() === 'true' + + if (model.includes('veo-3.0-fast-generate-001')) { + return generateAudio ? '$3.20/Run' : '$2.00/Run' + } else if (model.includes('veo-3.0-generate-001')) { + return generateAudio ? '$6.00/Run' : '$4.00/Run' + } + + // Default fallback + return '$2.00-6.00/Run' + } + }, LumaImageNode: { displayPrice: (node: LGraphNode): string => { const modelWidget = node.widgets?.find( @@ -1340,6 +1367,7 @@ export const useNodePricing = () => { FluxProKontextProNode: [], FluxProKontextMaxNode: [], VeoVideoGenerationNode: ['duration_seconds'], + Veo3VideoGenerationNode: ['model', 'generate_audio'], LumaVideoNode: ['model', 'resolution', 'duration'], LumaImageToVideoNode: ['model', 'resolution', 'duration'], LumaImageNode: ['model', 'aspect_ratio'], diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index 9b420baaa8..f73609c1ab 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -813,7 +813,7 @@ export const CORE_SETTINGS: SettingParams[] = [ id: 'Comfy.Minimap.Visible', name: 'Display minimap on canvas', type: 'hidden', - defaultValue: false, + defaultValue: true, versionAdded: '1.25.0' }, { diff --git a/src/stores/releaseStore.ts b/src/stores/releaseStore.ts index 321204d9ae..b795fbca18 100644 --- a/src/stores/releaseStore.ts +++ b/src/stores/releaseStore.ts @@ -4,6 +4,7 @@ import { computed, ref } from 'vue' import { type ReleaseNote, useReleaseService } from '@/services/releaseService' import { useSettingStore } from '@/stores/settingStore' import { useSystemStatsStore } from '@/stores/systemStatsStore' +import { isElectron } from '@/utils/envUtil' import { compareVersions, stringToLocale } from '@/utils/formatUtil' // Store for managing release notes @@ -76,6 +77,11 @@ export const useReleaseStore = defineStore('release', () => { // Show toast if needed const shouldShowToast = computed(() => { + // Only show on desktop version + if (!isElectron()) { + return false + } + // Skip if notifications are disabled if (!showVersionUpdates.value) { return false @@ -103,6 +109,11 @@ export const useReleaseStore = defineStore('release', () => { // Show red-dot indicator const shouldShowRedDot = computed(() => { + // Only show on desktop version + if (!isElectron()) { + return false + } + // Skip if notifications are disabled if (!showVersionUpdates.value) { return false @@ -145,6 +156,11 @@ export const useReleaseStore = defineStore('release', () => { // Show "What's New" popup const shouldShowPopup = computed(() => { + // Only show on desktop version + if (!isElectron()) { + return false + } + // Skip if notifications are disabled if (!showVersionUpdates.value) { return false diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 67f78d7d38..1633c79d14 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -393,6 +393,86 @@ describe('useNodePricing', () => { }) }) + describe('dynamic pricing - Veo3VideoGenerationNode', () => { + it('should return $2.00 for veo-3.0-fast-generate-001 without audio', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-fast-generate-001' }, + { name: 'generate_audio', value: false } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$2.00/Run') + }) + + it('should return $3.20 for veo-3.0-fast-generate-001 with audio', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-fast-generate-001' }, + { name: 'generate_audio', value: true } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$3.20/Run') + }) + + it('should return $4.00 for veo-3.0-generate-001 without audio', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-generate-001' }, + { name: 'generate_audio', value: false } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$4.00/Run') + }) + + it('should return $6.00 for veo-3.0-generate-001 with audio', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-generate-001' }, + { name: 'generate_audio', value: true } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$6.00/Run') + }) + + it('should return range when widgets are missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', []) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$2.00-6.00/Run (varies with model & audio generation)' + ) + }) + + it('should return range when only model widget is present', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-generate-001' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$2.00-6.00/Run (varies with model & audio generation)' + ) + }) + + it('should return range when only generate_audio widget is present', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'generate_audio', value: true } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$2.00-6.00/Run (varies with model & audio generation)' + ) + }) + }) + describe('dynamic pricing - LumaVideoNode', () => { it('should return $2.19 for ray-flash-2 4K 5s', () => { const { getNodeDisplayPrice } = useNodePricing() @@ -736,6 +816,13 @@ describe('useNodePricing', () => { expect(widgetNames).toEqual(['duration_seconds']) }) + it('should return correct widget names for Veo3VideoGenerationNode', () => { + const { getRelevantWidgetNames } = useNodePricing() + + const widgetNames = getRelevantWidgetNames('Veo3VideoGenerationNode') + expect(widgetNames).toEqual(['model', 'generate_audio']) + }) + it('should return correct widget names for LumaVideoNode', () => { const { getRelevantWidgetNames } = useNodePricing() diff --git a/tests-ui/tests/store/releaseStore.test.ts b/tests-ui/tests/store/releaseStore.test.ts index 940c575ba3..8e49a56af4 100644 --- a/tests-ui/tests/store/releaseStore.test.ts +++ b/tests-ui/tests/store/releaseStore.test.ts @@ -5,6 +5,7 @@ import { useReleaseStore } from '@/stores/releaseStore' // Mock the dependencies vi.mock('@/utils/formatUtil') +vi.mock('@/utils/envUtil') vi.mock('@/services/releaseService') vi.mock('@/stores/settingStore') vi.mock('@/stores/systemStatsStore') @@ -56,10 +57,12 @@ describe('useReleaseStore', () => { const { useReleaseService } = await import('@/services/releaseService') const { useSettingStore } = await import('@/stores/settingStore') const { useSystemStatsStore } = await import('@/stores/systemStatsStore') + const { isElectron } = await import('@/utils/envUtil') vi.mocked(useReleaseService).mockReturnValue(mockReleaseService) vi.mocked(useSettingStore).mockReturnValue(mockSettingStore) vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore) + vi.mocked(isElectron).mockReturnValue(true) // Default showVersionUpdates to true mockSettingStore.get.mockImplementation((key: string) => { @@ -444,4 +447,118 @@ describe('useReleaseStore', () => { expect(mockReleaseService.getReleases).toHaveBeenCalledTimes(1) }) }) + + describe('isElectron environment checks', () => { + beforeEach(() => { + // Set up a new version available + store.releases = [mockRelease] + mockSettingStore.get.mockImplementation((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return true + return null + }) + }) + + describe('when running in Electron (desktop)', () => { + beforeEach(async () => { + const { isElectron } = await import('@/utils/envUtil') + vi.mocked(isElectron).mockReturnValue(true) + }) + + it('should show toast when conditions are met', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(1) + + // Need multiple releases for hasMediumOrHighAttention + const mediumRelease = { + ...mockRelease, + id: 2, + attention: 'medium' as const + } + store.releases = [mockRelease, mediumRelease] + + expect(store.shouldShowToast).toBe(true) + }) + + it('should show red dot when new version available', 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) + }) + }) + + describe('when NOT running in Electron (web)', () => { + beforeEach(async () => { + const { isElectron } = await import('@/utils/envUtil') + vi.mocked(isElectron).mockReturnValue(false) + }) + + it('should NOT show toast even when all other conditions are met', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(1) + + // Set up all conditions that would normally show toast + const mediumRelease = { + ...mockRelease, + id: 2, + attention: 'medium' as const + } + store.releases = [mockRelease, mediumRelease] + + expect(store.shouldShowToast).toBe(false) + }) + + it('should NOT show red dot even when new version available', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(1) + + expect(store.shouldShowRedDot).toBe(false) + }) + + it('should NOT show toast regardless of attention level', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).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', async () => { + const { compareVersions } = await import('@/utils/formatUtil') + vi.mocked(compareVersions).mockReturnValue(1) + + store.releases = [{ ...mockRelease, attention: 'high' as const }] + + 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) + }) + }) + }) })