mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-25 00:39:49 +00:00
style: update ui and design of system notification components (what's new, new release notification, help center) (#6300)
## Summary Migrated help center and release notification components from hardcoded colors to semantic design tokens for automatic light/dark theme support. <img width="808" height="874" alt="Selection_2298" src="https://github.com/user-attachments/assets/c7fb956e-700b-49df-bba0-b85705e89ce7" /> <img width="852" height="710" alt="Selection_2265" src="https://github.com/user-attachments/assets/618205e1-5068-499d-80ab-72626b32d7e1" /> <img width="493" height="838" alt="Screenshot from 2025-10-25 21-46-11" src="https://github.com/user-attachments/assets/7b696673-ec19-4a16-a0b5-ca744ae62fe1" /> <img width="493" height="838" alt="Screenshot from 2025-10-25 21-46-25" src="https://github.com/user-attachments/assets/2767d722-a0e1-426d-82d9-6d5a59f373ee" /> ## Changes - **What**: Replaced hardcoded hex/rgb colors with semantic tokens in HelpCenterMenuContent, WhatsNewPopup, and ReleaseNotificationToast components - **Design System**: Added `--interface-menu-surface` and `--interface-menu-stroke` tokens to style.css for consistent menu theming - **UX**: Updated help center menu structure - added "Give Feedback" button, renamed "Help & Feedback" to "Help & Support", switched to Lucide icons (except Discord brand logo), added external-link icons ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6300-style-update-ui-and-design-of-system-notification-components-what-s-new-new-release-no-2986d73d365081238458ea7d304b641e) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
278
src/platform/updates/components/ReleaseNotificationToast.test.ts
Normal file
278
src/platform/updates/components/ReleaseNotificationToast.test.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
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'
|
||||
|
||||
// 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
|
||||
})
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/formatUtil', () => ({
|
||||
formatVersionAnchor: vi.fn((version: string) => version.replace(/\./g, ''))
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/markdownRendererUtil', () => ({
|
||||
renderMarkdownToHtml: vi.fn((content: string) => `<div>${content}</div>`)
|
||||
}))
|
||||
|
||||
// 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('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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user