diff --git a/browser_tests/tests/releaseNotifications.spec.ts b/browser_tests/tests/releaseNotifications.spec.ts index 19d09327d0..a674824768 100644 --- a/browser_tests/tests/releaseNotifications.spec.ts +++ b/browser_tests/tests/releaseNotifications.spec.ts @@ -50,7 +50,7 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section shows the release - const whatsNewSection = comfyPage.page.locator('.whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') await expect(whatsNewSection).toBeVisible() // Should show the release version @@ -79,7 +79,7 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section shows no releases - const whatsNewSection = comfyPage.page.locator('.whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') await expect(whatsNewSection).toBeVisible() // Should show "No recent releases" message @@ -125,7 +125,7 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Should show no releases due to error - const whatsNewSection = comfyPage.page.locator('.whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') await expect( whatsNewSection.locator('text=No recent releases') ).toBeVisible() @@ -175,7 +175,7 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section is hidden - const whatsNewSection = comfyPage.page.locator('.whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') await expect(whatsNewSection).not.toBeVisible() // Should not show any popups or toasts @@ -263,7 +263,7 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section is visible - const whatsNewSection = comfyPage.page.locator('.whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') await expect(whatsNewSection).toBeVisible() // Should show the release @@ -311,7 +311,7 @@ test.describe('Release Notifications', () => { await helpCenterButton.click() // Verify "What's New?" section is visible - const whatsNewSection = comfyPage.page.locator('.whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') await expect(whatsNewSection).toBeVisible() // Close help center @@ -362,7 +362,7 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Section should be hidden regardless of empty releases - const whatsNewSection = comfyPage.page.locator('.whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') await expect(whatsNewSection).not.toBeVisible() }) }) diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index 503e065e48..e0b13566cc 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -89,6 +89,13 @@ --color-danger-100: #c02323; --color-danger-200: #d62952; + /* Light theme specific tokens */ + --color-interface-menu-keybind-surface-blue: #78BAE9; + + /* Corner radius tokens */ + --corner-radius-corner-radius-md: 8px; + --base-corner-radius-corner-radius-md: 8px; + --color-coral-red-600: #973a40; --color-coral-red-500: #c53f49; --color-coral-red-400: #dd424e; @@ -181,9 +188,18 @@ --interface-menu-component-surface-hovered: var(--color-smoke-200); --interface-menu-component-surface-selected: var(--color-smoke-400); --interface-menu-keybind-surface-default: var(--color-smoke-500); + --interface-menu-surface: var(--color-white); + --interface-menu-stroke: var(--color-smoke-600); --interface-panel-surface: var(--color-white); --interface-stroke: var(--color-smoke-300); + /* Light theme specific tokens */ + --color-interface-menu-keybind-surface-blue: #78BAE9; + + /* Corner radius tokens */ + --corner-radius-corner-radius-md: 8px; + --base-corner-radius-corner-radius-md: 8px; + --nav-background: var(--color-white); --node-border: var(--color-smoke-300); @@ -293,6 +309,8 @@ --interface-menu-component-surface-hovered: var(--color-charcoal-400); --interface-menu-component-surface-selected: var(--color-charcoal-300); --interface-menu-keybind-surface-default: var(--color-charcoal-200); + --interface-menu-surface: var(--color-charcoal-800); + --interface-menu-stroke: var(--color-ash-800); --interface-panel-surface: var(--color-charcoal-800); --interface-stroke: var(--color-charcoal-400); @@ -383,6 +401,8 @@ --color-interface-menu-keybind-surface-default: var( --interface-menu-keybind-surface-default ); + --color-interface-menu-surface: var(--interface-menu-surface); + --color-interface-menu-stroke: var(--interface-menu-stroke); --color-interface-panel-surface: var(--interface-panel-surface); --color-interface-panel-hover-surface: var(--interface-panel-hover-surface); --color-interface-panel-selected-surface: var( diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue index e028a3eb1e..c266c7ce9b 100644 --- a/src/components/helpcenter/HelpCenterMenuContent.vue +++ b/src/components/helpcenter/HelpCenterMenuContent.vue @@ -1,38 +1,54 @@ diff --git a/src/platform/updates/components/WhatsNewPopup.test.ts b/src/platform/updates/components/WhatsNewPopup.test.ts new file mode 100644 index 0000000000..24e5fb23e6 --- /dev/null +++ b/src/platform/updates/components/WhatsNewPopup.test.ts @@ -0,0 +1,216 @@ +import type { VueWrapper } from '@vue/test-utils' +import { mount } from '@vue/test-utils' +import Button from 'primevue/button' +import PrimeVue from 'primevue/config' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { ReleaseNote } from '../common/releaseService' +import WhatsNewPopup from './WhatsNewPopup.vue' + +// Mock dependencies +vi.mock('@/i18n', () => ({ + t: (key: string, params?: Record) => { + const translations: Record = { + 'g.close': 'Close', + 'whatsNewPopup.later': 'Later', + 'whatsNewPopup.update': 'Update', + 'whatsNewPopup.learnMore': 'Learn More', + 'whatsNewPopup.noReleaseNotes': 'No release notes available' + } + return params + ? `${translations[key] || key}:${JSON.stringify(params)}` + : translations[key] || key + }, + d: (date: Date) => date.toLocaleDateString() +})) + +vi.mock('vue-i18n', () => ({ + useI18n: vi.fn(() => ({ + locale: { value: 'en' }, + t: vi.fn((key: string) => { + const translations: Record = { + 'g.close': 'Close', + 'whatsNewPopup.later': 'Later', + 'whatsNewPopup.update': 'Update', + 'whatsNewPopup.learnMore': 'Learn More', + 'whatsNewPopup.noReleaseNotes': 'No release notes available' + } + 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) => `
${content}
`) +})) + +// Mock release store +const mockReleaseStore = { + recentRelease: null as ReleaseNote | null, + shouldShowPopup: false, + handleWhatsNewSeen: vi.fn(), + releases: [] as ReleaseNote[], + fetchReleases: vi.fn() +} + +vi.mock('../common/releaseStore', () => ({ + useReleaseStore: vi.fn(() => mockReleaseStore) +})) + +describe('WhatsNewPopup', () => { + let wrapper: VueWrapper + + const mountComponent = (props = {}) => { + return mount(WhatsNewPopup, { + global: { + plugins: [PrimeVue], + components: { Button }, + mocks: { + $t: (key: string) => { + const translations: Record = { + 'g.close': 'Close', + 'whatsNewPopup.later': 'Later', + 'whatsNewPopup.update': 'Update', + 'whatsNewPopup.learnMore': 'Learn More', + 'whatsNewPopup.noReleaseNotes': 'No release notes available' + } + return translations[key] || key + } + }, + stubs: { + // Stub Lucide icons + 'i-lucide-x': true, + 'i-lucide-external-link': true + } + }, + props + }) + } + + beforeEach(() => { + vi.clearAllMocks() + // Reset store state + mockReleaseStore.recentRelease = null + mockReleaseStore.shouldShowPopup = false + }) + + it('renders correctly when shouldShow is true', () => { + mockReleaseStore.shouldShowPopup = true + mockReleaseStore.recentRelease = { + version: '1.2.3', + content: '# Test Release\n\nSome content' + } as ReleaseNote + + wrapper = mountComponent() + expect(wrapper.find('.whats-new-popup').exists()).toBe(true) + }) + + it('does not render when shouldShow is false', () => { + mockReleaseStore.shouldShowPopup = false + wrapper = mountComponent() + expect(wrapper.find('.whats-new-popup').exists()).toBe(false) + }) + + it('calls handleWhatsNewSeen when close button is clicked', async () => { + mockReleaseStore.shouldShowPopup = true + mockReleaseStore.recentRelease = { + version: '1.2.3', + content: '# Test Release' + } as ReleaseNote + + wrapper = mountComponent() + + const closeButton = wrapper.findComponent(Button) + await closeButton.trigger('click') + + expect(mockReleaseStore.handleWhatsNewSeen).toHaveBeenCalledWith('1.2.3') + }) + + it('generates correct changelog URL', () => { + mockReleaseStore.shouldShowPopup = true + mockReleaseStore.recentRelease = { + version: '1.2.3', + content: '# Test Release' + } as ReleaseNote + + wrapper = mountComponent() + + const learnMoreLink = wrapper.find('.learn-more-link') + expect(learnMoreLink.attributes('href')).toContain( + 'docs.comfy.org/changelog' + ) + }) + + it('handles missing release content gracefully', () => { + mockReleaseStore.shouldShowPopup = true + mockReleaseStore.recentRelease = { + version: '1.2.3', + content: '' + } as ReleaseNote + + wrapper = mountComponent() + + // Should render fallback content + const contentElement = wrapper.find('.content-text') + expect(contentElement.exists()).toBe(true) + }) + + it('emits whats-new-dismissed event when popup is closed', async () => { + mockReleaseStore.shouldShowPopup = true + mockReleaseStore.recentRelease = { + version: '1.2.3', + content: '# Test Release' + } as ReleaseNote + + wrapper = mountComponent() + + // Call the close method directly instead of triggering DOM event + await (wrapper.vm as any).closePopup() + + expect(wrapper.emitted('whats-new-dismissed')).toBeTruthy() + }) + + it('fetches releases on mount when not already loaded', async () => { + mockReleaseStore.shouldShowPopup = true + mockReleaseStore.releases = [] // Empty releases array + + wrapper = mountComponent() + + expect(mockReleaseStore.fetchReleases).toHaveBeenCalled() + }) + + it('does not fetch releases when already loaded', async () => { + mockReleaseStore.shouldShowPopup = true + mockReleaseStore.releases = [{ version: '1.0.0' } as ReleaseNote] // Non-empty releases array + + wrapper = mountComponent() + + expect(mockReleaseStore.fetchReleases).not.toHaveBeenCalled() + }) + + it('processes markdown content correctly', async () => { + const mockMarkdownRendererModule = await vi.importMock( + '@/utils/markdownRendererUtil' + ) + const mockMarkdownRenderer = vi.mocked(mockMarkdownRendererModule) + .renderMarkdownToHtml as any + mockMarkdownRenderer.mockReturnValue('

Processed Content

') + + mockReleaseStore.shouldShowPopup = true + mockReleaseStore.recentRelease = { + version: '1.2.3', + content: '# Original Title\n\nContent' + } as ReleaseNote + + wrapper = mountComponent() + + // Should call markdown renderer with original content (no modification) + expect(mockMarkdownRenderer).toHaveBeenCalledWith( + '# Original Title\n\nContent' + ) + }) +}) diff --git a/src/platform/updates/components/WhatsNewPopup.vue b/src/platform/updates/components/WhatsNewPopup.vue index f1a5060b57..47f3f321ae 100644 --- a/src/platform/updates/components/WhatsNewPopup.vue +++ b/src/platform/updates/components/WhatsNewPopup.vue @@ -1,69 +1,56 @@ @@ -170,165 +167,72 @@ defineExpose({ pointer-events: auto; } -/* Arrow pointing to help center */ -.help-center-arrow { - position: absolute; - bottom: calc( - var(--sidebar-width) * 2 + var(--sidebar-width) / 2 - ); /* Position to center of help center icon (2 icons below + half icon height for center) */ - transform: none; - z-index: 999; - pointer-events: none; -} - -/* Position arrow based on sidebar location */ -.whats-new-popup-container.sidebar-left .help-center-arrow { - left: -14px; /* Overlap with popup outline */ -} - -.whats-new-popup-container.sidebar-left.small-sidebar .help-center-arrow { - left: -14px; /* Overlap with popup outline */ - bottom: calc( - var(--sidebar-width) * 2 + var(--sidebar-icon-size) / 2 - - var(--whats-new-popup-bottom) - ); /* Position to center of help center icon (2 icons below + half icon height for center - what's new popup bottom position ) */ -} - -/* Sidebar positioning classes applied by parent */ -.whats-new-popup-container.sidebar-left { - left: 1rem; -} - -.whats-new-popup-container.sidebar-left.small-sidebar { - left: 1rem; -} - -.whats-new-popup-container.sidebar-right { - right: 1rem; -} - .whats-new-popup { - background: #353535; - border-radius: 12px; + background: var(--interface-menu-surface); + border-radius: var(--corner-radius-corner-radius-md, 8px); max-width: 400px; width: 400px; - outline: 1px solid #4e4e4e; - outline-offset: -1px; - box-shadow: 0 8px 32px rgb(0 0 0 / 0.3); + border: 1px solid var(--interface-menu-stroke); + box-shadow: 1px 1px 8px 0 rgb(0 0 0 / 0.2); position: relative; -} - -/* Content Section */ -.popup-content { display: flex; flex-direction: column; - gap: 24px; - max-height: 80vh; - overflow-y: auto; - padding: 32px 32px 24px; - border-radius: 12px; } -/* Close button */ -.close-button { - position: absolute; - top: 0; - right: 0; - width: 32px; - height: 32px; - padding: 6px; - background: #7c7c7c; - border-radius: 16px; - border: none; - cursor: pointer; +/* Modal Body */ +.modal-body { display: flex; - justify-content: center; - align-items: center; - transform: translate(30%, -30%); - transition: - background-color 0.2s ease, - transform 0.1s ease; - z-index: 1; -} - -.close-button:hover { - background: #8e8e8e; -} - -.close-button:active { - background: #6a6a6a; - transform: translate(30%, -30%) scale(0.95); -} - -.close-icon { - width: 16px; - height: 16px; - position: relative; - opacity: 0.9; - transition: opacity 0.2s ease; -} - -.close-button:hover .close-icon { - opacity: 1; -} - -.close-icon::before, -.close-icon::after { - content: ''; - position: absolute; - width: 12px; - height: 2px; - background: white; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) rotate(45deg); - transition: background-color 0.2s ease; + flex-direction: column; + gap: 1rem; + padding: 0; + flex: 1; } -.close-icon::after { - transform: translate(-50%, -50%) rotate(-45deg); +.modal-header { + display: flex; + flex-direction: column; + gap: 8px; } .content-text { - color: white; + color: var(--text-primary); font-size: 14px; line-height: 1.5; word-wrap: break-word; + padding: 0 1rem; } /* Style the markdown content */ /* Title */ .content-text :deep(*) { box-sizing: border-box; - margin: 0; - padding: 0; } .content-text :deep(h1) { - font-size: 16px; - font-weight: 700; - margin-bottom: 8px; + color: var(--text-primary); + font-family: Inter, sans-serif; + font-size: 14px; + margin-top: 1rem; + margin-bottom: 1rem; } /* Version subtitle - targets the first p tag after h1 */ .content-text :deep(h1 + p) { - color: #c0c0c0; - font-size: 16px; - font-weight: 500; - margin-bottom: 16px; - opacity: 0.8; + color: var(--text-secondary); + font-family: Inter, sans-serif; } /* Regular paragraphs - short description */ .content-text :deep(p) { - margin-bottom: 16px; - color: #e0e0e0; + color: var(--text-secondary); + font-family: Inter, sans-serif; + margin: 1rem 0; } /* List */ .content-text :deep(ul), .content-text :deep(ol) { - margin-bottom: 16px; + margin-bottom: 0; padding-left: 0; list-style: none; } @@ -343,110 +247,156 @@ defineExpose({ margin-bottom: 0; } -/* List items */ .content-text :deep(li) { - margin-bottom: 8px; + margin-bottom: 6px; position: relative; - padding-left: 20px; + padding-left: 18px; + color: var(--text-secondary); + font-family: Inter, sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 1.2102; } .content-text :deep(li:last-child) { margin-bottom: 0; } -/* Custom bullet points */ .content-text :deep(li::before) { content: ''; position: absolute; - left: 0; - top: 10px; - display: flex; - width: 8px; - height: 8px; - justify-content: center; - align-items: center; - aspect-ratio: 1/1; - border-radius: 100px; - background: #60a5fa; + left: 4px; + top: 7px; + width: 6px; + height: 6px; + border: 2px solid var(--text-secondary); + border-radius: 50%; + background: transparent; } -/* List item strong text */ .content-text :deep(li strong) { - color: #fff; + color: var(--text-secondary); + font-family: Inter, sans-serif; font-size: 14px; - display: block; - margin-bottom: 4px; + font-weight: 400; + line-height: 1.2102; + margin-right: 4px; } .content-text :deep(li p) { - font-size: 12px; - margin-bottom: 0; - line-height: 2; + margin: 2px 0 0; + display: inline; } /* Code styling */ .content-text :deep(code) { - background-color: #2a2a2a; - border: 1px solid #4a4a4a; + background-color: var(--input-surface); + border: 1px solid var(--interface-menu-stroke); border-radius: 4px; padding: 2px 6px; - color: #f8f8f2; + color: var(--text-primary); white-space: nowrap; } -/* Remove top margin for first media element */ -.content-text :deep(img:first-child), -.content-text :deep(video:first-child), -.content-text :deep(iframe:first-child) { - margin-top: -32px; /* Align with the top edge of the popup content */ - margin-bottom: 24px; +.content-text :deep(img) { + width: 100%; + height: 200px; + margin: 0 0 16px; + object-fit: cover; + display: block; + border-radius: 8px; } -/* Media elements */ -.content-text :deep(img), -.content-text :deep(video), -.content-text :deep(iframe) { - width: calc(100% + 64px); - height: auto; - margin: 24px -32px; - display: block; +.content-text :deep(img:first-child) { + margin: -1rem -1rem 16px; + width: calc(100% + 2rem); + border-top-left-radius: var(--corner-radius-corner-radius-md, 8px); + border-top-right-radius: var(--corner-radius-corner-radius-md, 8px); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.content-text :deep(img + h1) { + margin-top: 0; +} + +.content-text :deep(h2) { + color: var(--text-primary); + font-family: Inter, sans-serif; + font-size: 16px; + font-weight: 600; + margin: 16px 0 8px; + line-height: 1.4; } -/* Actions Section */ -.popup-actions { +/* Modal Footer */ +.modal-footer { display: flex; justify-content: space-between; align-items: center; - gap: 8px; + gap: 16px; + padding: 16px; + border-top: none; +} + +.footer-actions { + display: flex; + align-items: center; + gap: 16px; } .learn-more-link { - color: #60a5fa; + display: flex; + align-items: center; + gap: 8px; + color: var(--text-secondary); font-size: 14px; - font-weight: 500; - line-height: 18.2px; + font-weight: 400; + line-height: 1.2102; text-decoration: none; + padding: 4px 0; } .learn-more-link:hover { - text-decoration: underline; + color: var(--text-primary); +} + +.learn-more-link i { + width: 16px; + height: 16px; +} + +.action-secondary { + height: 32px; + padding: 4px 0; + background: transparent; + border: none; + color: var(--text-secondary); + font-size: 14px; + font-weight: 400; + line-height: 1.2102; + cursor: pointer; + border-radius: 4px; +} + +.action-secondary:hover { + color: var(--text-primary); } -.cta-button { +.action-primary { height: 40px; - padding: 0 20px; - background: white; - border-radius: 6px; - outline: 1px solid #4e4e4e; - outline-offset: -1px; + padding: 8px 16px; + background: var(--interface-menu-component-surface-hovered); + border-radius: var(--base-corner-radius-corner-radius-md, 8px); border: none; - color: #121212; + color: var(--text-primary); font-size: 14px; - font-weight: 500; + font-weight: 400; + line-height: 1.2102; cursor: pointer; } -.cta-button:hover { - background: #f0f0f0; +.action-primary:hover { + background: var(--button-hover-surface); } diff --git a/tests-ui/tests/components/helpcenter/WhatsNewPopup.test.ts b/tests-ui/tests/components/helpcenter/WhatsNewPopup.test.ts deleted file mode 100644 index 0f8adeb609..0000000000 --- a/tests-ui/tests/components/helpcenter/WhatsNewPopup.test.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { mount } from '@vue/test-utils' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { nextTick } from 'vue' - -import WhatsNewPopup from '@/platform/updates/components/WhatsNewPopup.vue' -import type { components } from '@/types/comfyRegistryTypes' - -type ReleaseNote = components['schemas']['ReleaseNote'] - -// Mock dependencies -vi.mock('vue-i18n', () => ({ - useI18n: vi.fn(() => ({ - locale: { value: 'en' }, - t: vi.fn((key) => key) - })) -})) - -vi.mock('@/utils/markdownRendererUtil', () => ({ - renderMarkdownToHtml: vi.fn((content) => `

${content}

`) -})) - -vi.mock('@/platform/updates/common/releaseStore', () => ({ - useReleaseStore: vi.fn() -})) - -describe('WhatsNewPopup', () => { - const mockReleaseStore = { - recentRelease: null as ReleaseNote | null, - shouldShowPopup: false, - handleWhatsNewSeen: vi.fn(), - releases: [] as ReleaseNote[], - fetchReleases: vi.fn() - } - - const createWrapper = (props = {}) => { - return mount(WhatsNewPopup, { - props, - global: { - mocks: { - $t: vi.fn((key: string) => { - const translations: Record = { - 'g.close': 'Close', - 'whatsNewPopup.noReleaseNotes': 'No release notes available' - } - return translations[key] || key - }) - } - } - }) - } - - beforeEach(async () => { - vi.clearAllMocks() - - // Reset mock store - mockReleaseStore.recentRelease = null - mockReleaseStore.shouldShowPopup = false - mockReleaseStore.releases = [] - - // Mock release store - const { useReleaseStore } = await import( - '@/platform/updates/common/releaseStore' - ) - vi.mocked(useReleaseStore).mockReturnValue(mockReleaseStore as any) - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('visibility', () => { - it('should not show when shouldShowPopup is false', () => { - mockReleaseStore.shouldShowPopup = false - - const wrapper = createWrapper() - - expect(wrapper.find('.whats-new-popup-container').exists()).toBe(false) - }) - - it('should show when shouldShowPopup is true and not dismissed', () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: 'New features added', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - - expect(wrapper.find('.whats-new-popup-container').exists()).toBe(true) - expect(wrapper.find('.whats-new-popup').exists()).toBe(true) - }) - - it('should hide when dismissed locally', async () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: 'New features added', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - - // Initially visible - expect(wrapper.find('.whats-new-popup-container').exists()).toBe(true) - - // Click close button - await wrapper.find('.close-button').trigger('click') - - // Should be hidden - expect(wrapper.find('.whats-new-popup-container').exists()).toBe(false) - }) - }) - - describe('content rendering', () => { - it('should render release content using renderMarkdownToHtml', async () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: '# Release Notes\n\nNew features', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - - // Check that the content is rendered (renderMarkdownToHtml is mocked to return processed content) - expect(wrapper.find('.content-text').exists()).toBe(true) - const contentHtml = wrapper.find('.content-text').html() - expect(contentHtml).toContain('

# Release Notes') - }) - - it('should handle missing release content', () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: '', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - - expect(wrapper.find('.content-text').html()).toContain( - 'whatsNewPopup.noReleaseNotes' - ) - }) - - it('should handle markdown parsing errors gracefully', () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: 'Content with\nnewlines', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - - // Should show content even without markdown processing - expect(wrapper.find('.content-text').exists()).toBe(true) - }) - }) - - describe('changelog URL generation', () => { - it('should generate English changelog URL with version anchor', () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0-beta.1', - attention: 'medium', - content: 'Release content', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - const learnMoreLink = wrapper.find('.learn-more-link') - - // formatVersionAnchor replaces dots with dashes: 1.24.0-beta.1 -> v1-24-0-beta-1 - expect(learnMoreLink.attributes('href')).toBe( - 'https://docs.comfy.org/changelog#v1-24-0-beta-1' - ) - }) - - it('should generate Chinese changelog URL when locale is zh', () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: 'Release content', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper({ - global: { - mocks: { - $t: vi.fn((key: string) => { - const translations: Record = { - 'g.close': 'Close', - 'whatsNewPopup.noReleaseNotes': 'No release notes available', - 'whatsNewPopup.learnMore': 'Learn More' - } - return translations[key] || key - }) - }, - provide: { - // Mock vue-i18n locale as Chinese - locale: { value: 'zh' } - } - } - }) - - // Since the locale mocking doesn't work well in tests, just check the English URL for now - // In a real component test with proper i18n setup, this would show the Chinese URL - const learnMoreLink = wrapper.find('.learn-more-link') - expect(learnMoreLink.attributes('href')).toBe( - 'https://docs.comfy.org/changelog#v1-24-0' - ) - }) - - it('should generate base changelog URL when no version available', () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '', - attention: 'medium', - content: 'Release content', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - const learnMoreLink = wrapper.find('.learn-more-link') - - expect(learnMoreLink.attributes('href')).toBe( - 'https://docs.comfy.org/changelog' - ) - }) - }) - - describe('popup dismissal', () => { - it('should call handleWhatsNewSeen and emit event when closed', async () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: 'Release content', - published_at: '2023-01-01T00:00:00Z' - } - mockReleaseStore.handleWhatsNewSeen.mockResolvedValue(undefined) - - const wrapper = createWrapper() - - // Click close button - await wrapper.find('.close-button').trigger('click') - - expect(mockReleaseStore.handleWhatsNewSeen).toHaveBeenCalledWith('1.24.0') - expect(wrapper.emitted('whats-new-dismissed')).toBeTruthy() - expect(wrapper.emitted('whats-new-dismissed')).toHaveLength(1) - }) - - it('should close when learn more link is clicked', async () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: 'Release content', - published_at: '2023-01-01T00:00:00Z' - } - mockReleaseStore.handleWhatsNewSeen.mockResolvedValue(undefined) - - const wrapper = createWrapper() - - // Click learn more link - await wrapper.find('.learn-more-link').trigger('click') - - expect(mockReleaseStore.handleWhatsNewSeen).toHaveBeenCalledWith('1.24.0') - expect(wrapper.emitted('whats-new-dismissed')).toBeTruthy() - }) - - it('should handle cases where no release is available during close', async () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = null - - const wrapper = createWrapper() - - // Try to close - await wrapper.find('.close-button').trigger('click') - - expect(mockReleaseStore.handleWhatsNewSeen).not.toHaveBeenCalled() - expect(wrapper.emitted('whats-new-dismissed')).toBeTruthy() - }) - }) - - describe('exposed methods', () => { - it('should expose show and hide methods', () => { - const wrapper = createWrapper() - - expect(wrapper.vm.show).toBeDefined() - expect(wrapper.vm.hide).toBeDefined() - expect(typeof wrapper.vm.show).toBe('function') - expect(typeof wrapper.vm.hide).toBe('function') - }) - - it('should show popup when show method is called', async () => { - mockReleaseStore.shouldShowPopup = true - - const wrapper = createWrapper() - - // Initially hide it - wrapper.vm.hide() - await nextTick() - expect(wrapper.find('.whats-new-popup-container').exists()).toBe(false) - - // Show it - wrapper.vm.show() - await nextTick() - expect(wrapper.find('.whats-new-popup-container').exists()).toBe(true) - }) - - it('should hide popup when hide method is called', async () => { - mockReleaseStore.shouldShowPopup = true - - const wrapper = createWrapper() - - // Initially visible - expect(wrapper.find('.whats-new-popup-container').exists()).toBe(true) - - // Hide it - wrapper.vm.hide() - await nextTick() - expect(wrapper.find('.whats-new-popup-container').exists()).toBe(false) - }) - }) - - describe('initialization', () => { - it('should fetch releases on mount if not already loaded', async () => { - mockReleaseStore.releases = [] - mockReleaseStore.fetchReleases.mockResolvedValue(undefined) - - createWrapper() - - // Wait for onMounted - await nextTick() - - expect(mockReleaseStore.fetchReleases).toHaveBeenCalled() - }) - - it('should not fetch releases if already loaded', async () => { - mockReleaseStore.releases = [ - { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium' as const, - content: 'Content', - published_at: '2023-01-01T00:00:00Z' - } - ] - mockReleaseStore.fetchReleases.mockResolvedValue(undefined) - - createWrapper() - - // Wait for onMounted - await nextTick() - - expect(mockReleaseStore.fetchReleases).not.toHaveBeenCalled() - }) - }) - - describe('accessibility', () => { - it('should have proper aria-label for close button', () => { - const mockT = vi.fn((key) => (key === 'g.close' ? 'Close' : key)) - vi.doMock('vue-i18n', () => ({ - useI18n: vi.fn(() => ({ - locale: { value: 'en' }, - t: mockT - })) - })) - - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: 'Content', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - - expect(wrapper.find('.close-button').attributes('aria-label')).toBe( - 'Close' - ) - }) - - it('should have proper link attributes for external changelog', () => { - mockReleaseStore.shouldShowPopup = true - mockReleaseStore.recentRelease = { - id: 1, - project: 'comfyui_frontend', - version: '1.24.0', - attention: 'medium', - content: 'Content', - published_at: '2023-01-01T00:00:00Z' - } - - const wrapper = createWrapper() - const learnMoreLink = wrapper.find('.learn-more-link') - - expect(learnMoreLink.attributes('target')).toBe('_blank') - expect(learnMoreLink.attributes('rel')).toBe('noopener,noreferrer') - }) - }) -})