Files
ComfyUI_frontend/src/platform/updates/components/ReleaseNotificationToast.test.ts
Johnpaul Chiwetelu 3946d7b5ff Road to no explicit any part 8 group 5 (#8329)
## Summary
- Add `createMockLLink` and `createMockLinks` factory functions to
handle hybrid Map/Record types
- Replace `as any` assertions with type-safe factory functions in
minimap tests
- Implement proper Pinia store mocking using `vi.hoisted()` pattern
- Remove unused `createMockSubgraph` export (shadowed by local
implementations)

## Test plan
- [x] All minimap tests pass (29 tests)
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] `pnpm knip` passes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8329-Road-to-no-explicit-any-part-8-group-5-2f56d73d365081218882de81d5526220)
by [Unito](https://www.unito.io)

---------

Co-authored-by: AustinMroz <austin@comfy.org>
2026-01-27 19:25:15 +01:00

360 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'
interface TestWindow extends Window {
electronAPI?: Record<string, unknown>
}
const { commandExecuteMock } = vi.hoisted(() => ({
commandExecuteMock: vi.fn()
}))
const { toastErrorHandlerMock } = vi.hoisted(() => ({
toastErrorHandlerMock: vi.fn()
}))
// 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()
// 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 in Electron', async () => {
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
})
;(window as TestWindow).electronAPI = {}
wrapper = mountComponent()
await wrapper.vm.handleUpdate()
expect(commandExecuteMock).toHaveBeenCalledWith(
'Comfy-Desktop.CheckForUpdates'
)
expect(mockWindowOpen).not.toHaveBeenCalled()
expect(toastErrorHandlerMock).not.toHaveBeenCalled()
delete (window as TestWindow).electronAPI
})
it('shows an error toast if the desktop updater flow fails in Electron', async () => {
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
})
;(window as TestWindow).electronAPI = {}
wrapper = mountComponent()
await wrapper.vm.handleUpdate()
expect(toastErrorHandlerMock).toHaveBeenCalledWith(error)
expect(mockWindowOpen).not.toHaveBeenCalled()
delete (window as TestWindow).electronAPI
})
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()
})
})