mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-09 17:40:09 +00:00
## Summary Replace all runtime `isElectron()` function calls with the build-time `isDesktop` constant from `@/platform/distribution/types`, enabling dead-code elimination in non-desktop builds. ## Changes - **What**: Migrate 30 files from runtime `isElectron()` detection (checking `window.electronAPI`) to the compile-time `isDesktop` constant (driven by `__DISTRIBUTION__` Vite define). Remove `isElectron` from `envUtil.ts`. Update `isNativeWindow()` to use `isDesktop`. Guard `electronAPI()` calls behind `isDesktop` checks in stores. Update 7 test files to use `vi.hoisted` + getter mock pattern for per-test `isDesktop` toggling. Add `DISTRIBUTION=desktop` to `dev:electron` script. ## Review Focus - The `electronDownloadStore.ts` now guards the top-level `electronAPI()` call behind `isDesktop` to prevent crashes on non-desktop builds. - Test mocking pattern uses `vi.hoisted` with a getter to allow per-test toggling of the `isDesktop` value. - Pre-existing issues not addressed: `as ElectronAPI` cast in `envUtil.ts`, `:class="[]"` in `BaseViewTemplate.vue`, `@ts-expect-error` in `ModelLibrarySidebarTab.vue`. - This subsumes PR #8627 and renders PR #6122 and PR #7374 obsolete. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8710-refactor-replace-runtime-isElectron-with-build-time-isDesktop-constant-3006d73d365081c08037f0e61c2f6c77) by [Unito](https://www.unito.io)
363 lines
10 KiB
TypeScript
363 lines
10 KiB
TypeScript
import type { VueWrapper } from '@vue/test-utils'
|
|
import { mount } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { ReleaseNote } from '../common/releaseService'
|
|
import ReleaseNotificationToast from './ReleaseNotificationToast.vue'
|
|
|
|
const mockData = vi.hoisted(() => ({ isDesktop: false }))
|
|
|
|
const { commandExecuteMock } = vi.hoisted(() => ({
|
|
commandExecuteMock: vi.fn()
|
|
}))
|
|
|
|
const { toastErrorHandlerMock } = vi.hoisted(() => ({
|
|
toastErrorHandlerMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@/platform/distribution/types', () => ({
|
|
isCloud: false,
|
|
isNightly: false,
|
|
get isDesktop() {
|
|
return mockData.isDesktop
|
|
}
|
|
}))
|
|
|
|
// Mock dependencies
|
|
vi.mock('vue-i18n', () => ({
|
|
useI18n: vi.fn(() => ({
|
|
locale: { value: 'en' },
|
|
t: vi.fn((key: string) => {
|
|
const translations: Record<string, string> = {
|
|
'releaseToast.newVersionAvailable': 'New update is out!',
|
|
'releaseToast.whatsNew': "See what's new",
|
|
'releaseToast.skip': 'Skip',
|
|
'releaseToast.update': 'Update',
|
|
'releaseToast.description':
|
|
'Check out the latest improvements and features in this update.'
|
|
}
|
|
return translations[key] || key
|
|
})
|
|
})),
|
|
createI18n: vi.fn(() => ({
|
|
global: {
|
|
locale: { value: 'en' }
|
|
}
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/utils/formatUtil', () => ({
|
|
formatVersionAnchor: vi.fn((version: string) => version.replace(/\./g, ''))
|
|
}))
|
|
|
|
vi.mock('@/utils/markdownRendererUtil', () => ({
|
|
renderMarkdownToHtml: vi.fn((content: string) => `<div>${content}</div>`)
|
|
}))
|
|
|
|
vi.mock('@/composables/useErrorHandling', () => ({
|
|
useErrorHandling: vi.fn(() => ({
|
|
toastErrorHandler: toastErrorHandlerMock
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/stores/commandStore', () => ({
|
|
useCommandStore: vi.fn(() => ({
|
|
execute: commandExecuteMock
|
|
}))
|
|
}))
|
|
|
|
// Mock release store
|
|
const mockReleaseStore = {
|
|
recentRelease: null as ReleaseNote | null,
|
|
shouldShowToast: false,
|
|
handleSkipRelease: vi.fn(),
|
|
handleShowChangelog: vi.fn(),
|
|
releases: [],
|
|
fetchReleases: vi.fn()
|
|
}
|
|
|
|
vi.mock('../common/releaseStore', () => ({
|
|
useReleaseStore: vi.fn(() => mockReleaseStore)
|
|
}))
|
|
|
|
describe('ReleaseNotificationToast', () => {
|
|
let wrapper: VueWrapper<InstanceType<typeof ReleaseNotificationToast>>
|
|
|
|
const mountComponent = (props = {}) => {
|
|
return mount(ReleaseNotificationToast, {
|
|
global: {
|
|
mocks: {
|
|
$t: (key: string) => {
|
|
const translations: Record<string, string> = {
|
|
'releaseToast.newVersionAvailable': 'New update is out!',
|
|
'releaseToast.whatsNew': "See what's new",
|
|
'releaseToast.skip': 'Skip',
|
|
'releaseToast.update': 'Update',
|
|
'releaseToast.description':
|
|
'Check out the latest improvements and features in this update.'
|
|
}
|
|
return translations[key] || key
|
|
}
|
|
},
|
|
stubs: {
|
|
// Stub Lucide icons
|
|
'i-lucide-rocket': true,
|
|
'i-lucide-external-link': true
|
|
}
|
|
},
|
|
props
|
|
})
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockData.isDesktop = false
|
|
// Reset store state
|
|
mockReleaseStore.recentRelease = null
|
|
mockReleaseStore.shouldShowToast = true // Force show for testing
|
|
})
|
|
|
|
it('renders correctly when shouldShow is true', () => {
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release\n\nSome content'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
expect(wrapper.find('.release-toast-popup').exists()).toBe(true)
|
|
})
|
|
|
|
it('displays rocket icon', () => {
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
expect(wrapper.find('.icon-\\[lucide--rocket\\]').exists()).toBe(true)
|
|
})
|
|
|
|
it('displays release version', () => {
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
expect(wrapper.text()).toContain('1.2.3')
|
|
})
|
|
|
|
it('calls handleSkipRelease when skip button is clicked', async () => {
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
|
|
const buttons = wrapper.findAll('button')
|
|
const skipButton = buttons.find(
|
|
(btn) =>
|
|
btn.text().includes('Skip') || btn.element.innerHTML.includes('skip')
|
|
)
|
|
expect(skipButton).toBeDefined()
|
|
await skipButton!.trigger('click')
|
|
|
|
expect(mockReleaseStore.handleSkipRelease).toHaveBeenCalledWith('1.2.3')
|
|
})
|
|
|
|
it('opens update URL when update button is clicked', async () => {
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
// Mock window.open
|
|
const mockWindowOpen = vi.fn()
|
|
Object.defineProperty(window, 'open', {
|
|
value: mockWindowOpen,
|
|
writable: true
|
|
})
|
|
|
|
wrapper = mountComponent()
|
|
|
|
// Call the handler directly instead of triggering DOM event
|
|
await wrapper.vm.handleUpdate()
|
|
|
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
|
'https://docs.comfy.org/installation/update_comfyui',
|
|
'_blank'
|
|
)
|
|
})
|
|
|
|
it('executes desktop updater flow when running on desktop', async () => {
|
|
mockData.isDesktop = true
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
commandExecuteMock.mockResolvedValueOnce(undefined)
|
|
|
|
const mockWindowOpen = vi.fn()
|
|
Object.defineProperty(window, 'open', {
|
|
value: mockWindowOpen,
|
|
writable: true
|
|
})
|
|
|
|
wrapper = mountComponent()
|
|
await wrapper.vm.handleUpdate()
|
|
|
|
expect(commandExecuteMock).toHaveBeenCalledWith(
|
|
'Comfy-Desktop.CheckForUpdates'
|
|
)
|
|
expect(mockWindowOpen).not.toHaveBeenCalled()
|
|
expect(toastErrorHandlerMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows an error toast if the desktop updater flow fails on desktop', async () => {
|
|
mockData.isDesktop = true
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
const error = new Error('Command Comfy-Desktop.CheckForUpdates not found')
|
|
commandExecuteMock.mockRejectedValueOnce(error)
|
|
|
|
const mockWindowOpen = vi.fn()
|
|
Object.defineProperty(window, 'open', {
|
|
value: mockWindowOpen,
|
|
writable: true
|
|
})
|
|
|
|
wrapper = mountComponent()
|
|
await wrapper.vm.handleUpdate()
|
|
|
|
expect(toastErrorHandlerMock).toHaveBeenCalledWith(error)
|
|
expect(mockWindowOpen).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls handleShowChangelog when learn more link is clicked', async () => {
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
|
|
// Call the handler directly instead of triggering DOM event
|
|
await wrapper.vm.handleLearnMore()
|
|
|
|
expect(mockReleaseStore.handleShowChangelog).toHaveBeenCalledWith('1.2.3')
|
|
})
|
|
|
|
it('generates correct changelog URL', () => {
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
|
|
const learnMoreLink = wrapper.find('a[target="_blank"]')
|
|
expect(learnMoreLink.exists()).toBe(true)
|
|
expect(learnMoreLink.attributes('href')).toContain(
|
|
'docs.comfy.org/changelog'
|
|
)
|
|
})
|
|
|
|
it('removes title from markdown content for toast display', async () => {
|
|
const mockMarkdownRendererModule = (await vi.importMock(
|
|
'@/utils/markdownRendererUtil'
|
|
)) as { renderMarkdownToHtml: ReturnType<typeof vi.fn> }
|
|
const mockMarkdownRenderer = vi.mocked(
|
|
mockMarkdownRendererModule.renderMarkdownToHtml
|
|
)
|
|
mockMarkdownRenderer.mockReturnValue('<div>Content without title</div>')
|
|
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release Title\n\nSome content'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
|
|
// Should call markdown renderer with title removed
|
|
expect(mockMarkdownRenderer).toHaveBeenCalledWith('\n\nSome content')
|
|
})
|
|
|
|
it('fetches releases on mount when not already loaded', async () => {
|
|
mockReleaseStore.releases = [] // Empty releases array
|
|
|
|
wrapper = mountComponent()
|
|
|
|
expect(mockReleaseStore.fetchReleases).toHaveBeenCalled()
|
|
})
|
|
|
|
it('handles missing release content gracefully', () => {
|
|
mockReleaseStore.shouldShowToast = true
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: ''
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
|
|
// Should render fallback content
|
|
const descriptionElement = wrapper.find('.pl-14')
|
|
expect(descriptionElement.exists()).toBe(true)
|
|
expect(descriptionElement.text()).toContain('Check out the latest')
|
|
})
|
|
|
|
it('auto-hides after timeout', async () => {
|
|
vi.useFakeTimers()
|
|
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
|
|
// Initially visible
|
|
expect(wrapper.find('.release-toast-popup').exists()).toBe(true)
|
|
|
|
// Fast-forward time to trigger auto-hide
|
|
vi.advanceTimersByTime(8000)
|
|
await wrapper.vm.$nextTick()
|
|
|
|
// Component should call dismissToast internally which hides it
|
|
// We can't test DOM visibility change because the component uses local state
|
|
// But we can verify the timer was set and would have triggered
|
|
expect(vi.getTimerCount()).toBe(0) // Timer should be cleared after auto-hide
|
|
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('clears auto-hide timer when manually dismissed', async () => {
|
|
vi.useFakeTimers()
|
|
|
|
mockReleaseStore.recentRelease = {
|
|
version: '1.2.3',
|
|
content: '# Test Release'
|
|
} as ReleaseNote
|
|
|
|
wrapper = mountComponent()
|
|
|
|
// Start the timer
|
|
vi.advanceTimersByTime(1000)
|
|
|
|
// Manually dismiss by calling handler directly
|
|
await wrapper.vm.handleSkip()
|
|
|
|
// Timer should be cleared
|
|
expect(vi.getTimerCount()).toBe(0)
|
|
|
|
// Verify the store method was called (manual dismissal)
|
|
expect(mockReleaseStore.handleSkipRelease).toHaveBeenCalled()
|
|
|
|
vi.useRealTimers()
|
|
})
|
|
})
|