mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-15 11:44:10 +00:00
## Summary Adds the frontend telemetry attribution needed to analyze settings, app-mode, and sharing funnel usage for MAR-321: re-enables three funnel events that were disabled by default, attaches app-mode/view-mode/dock-state context to UI click, run, and share events, and adds a per-session `shell_layout` snapshot plus right-side-panel toggle tracking. ## Changes - **What**: - Removes `setting_changed`, `template_filter_changed`, and `ui_button_click` from the code-default `DEFAULT_DISABLED_EVENTS` lists in the Mixpanel and PostHog providers, so these events now send by default (see deployment note). - `ui_button_click` now requires an `element_group`; all call sites are tagged (`sidebar`, `queue`, `actionbar`, `breadcrumb`, `error_dialog`, `errors_panel`, `graph_menu`, `graph_node`, `selection_toolbox`, `node_library`, `workflow_actions`, `cloud_notification`, `app_mode`, `top_menu`, `right_side_panel`) and the GTM provider forwards the field. - Run events (`run_button_clicked`, GTM `run_workflow`) now carry required `view_mode`/`is_app_mode` plus a new `dock_state` (`docked`/`floating`), read from the `Comfy.MenuPosition.Docked` localStorage key by a new `getActionbarDockState()` util. - Share funnel events (`share_flow`, `share_link_opened`, `shared_workflow_run`) now carry required `view_mode`/`is_app_mode`. A new `useShareFlowContext()` composable dedupes the source/view-mode context across the share dialog, URL copy field, and `useShareDialog`. GTM `share_flow` forwards the new fields and still omits `share_id`. - `shared_workflow_run` attribution is snapshotted onto the queued job at queue time, so switching app/graph mode while a job runs no longer misattributes the completion event (falls back to live values when no snapshot exists). - New `shell_layout` event fired once per session at graph-ready (cloud only): `view_mode`, `is_app_mode`, `dock_state`, `actionbar_position`, `active_sidebar_tab`, `right_side_panel_open`, `bottom_panel_open`, `open_workflow_tabs`. Forwarded by Mixpanel and PostHog; not sent to GTM. - The right side panel open button (top menu) and close button now fire `ui_button_click` (`right_side_panel_opened`/`right_side_panel_closed`), covering the panel open-rate gap. - **Dependencies**: None. ## Review Focus - `view_mode`/`is_app_mode` changed from optional to required (typed as `AppMode`) on run/share metadata — check no call sites were missed. - The queue-time snapshot in `executionStore` (`queuedJob.viewMode ?? mode.value`) and its regression test. - Share IDs remain limited to the providers/events that already carry share attribution (GTM still strips `share_id`). - `shell_layout` cadence is once per session (graph-ready idle callback), matching the gap analysis's "session snapshot" wording. Linear: MAR-321 Validation: - `pnpm test:unit src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts src/platform/telemetry/utils/getShellLayoutSnapshot.test.ts src/platform/workflow/sharing/components/ShareWorkflowDialogContent.test.ts src/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader.test.ts src/stores/executionStore.test.ts src/components/TopMenuSection.test.ts src/components/graph/selectionToolbox/InfoButton.test.ts src/components/rightSidePanel/errors/useErrorActions.test.ts src/views/GraphView.test.ts` - `pnpm typecheck` - `pnpm lint` - `pnpm knip` - `git diff --check` ## Deployment note `telemetry_disabled_events` is currently unset in the prod/staging/test dynamicconfig rows, so the code-default change here is what enables these events. The remote value remains available as a kill switch, but it **replaces** the code defaults rather than merging: if ops sets it to re-disable an event, the list must include every event that should stay disabled (`tab_count_tracking`, `node_search`, `node_search_result_selected`, `help_center_*`, `workflow_created`), not just the new ones.
639 lines
19 KiB
TypeScript
639 lines
19 KiB
TypeScript
/* eslint-disable testing-library/no-container */
|
|
/* eslint-disable testing-library/no-node-access */
|
|
import { createTestingPinia } from '@pinia/testing'
|
|
import { render, screen } from '@testing-library/vue'
|
|
import userEvent from '@testing-library/user-event'
|
|
import type { MenuItem } from 'primevue/menuitem'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { computed, defineComponent, h, nextTick, onMounted, ref } from 'vue'
|
|
import type { Component } from 'vue'
|
|
import { createI18n } from 'vue-i18n'
|
|
|
|
import TopMenuSection from '@/components/TopMenuSection.vue'
|
|
import QueueNotificationBannerHost from '@/components/queue/QueueNotificationBannerHost.vue'
|
|
import type {
|
|
JobListItem,
|
|
JobStatus
|
|
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useExecutionStore } from '@/stores/executionStore'
|
|
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
|
|
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
|
|
|
const mockData = vi.hoisted(() => ({
|
|
isLoggedIn: false,
|
|
isDesktop: false,
|
|
setShowConflictRedDot: (_value: boolean) => {}
|
|
}))
|
|
|
|
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
|
useCurrentUser: () => {
|
|
return {
|
|
isLoggedIn: computed(() => mockData.isLoggedIn)
|
|
}
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/platform/distribution/types', () => ({
|
|
isCloud: false,
|
|
isNightly: false,
|
|
get isDesktop() {
|
|
return mockData.isDesktop
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/platform/updates/common/releaseStore', () => ({
|
|
useReleaseStore: () => ({
|
|
shouldShowRedDot: computed(() => true)
|
|
})
|
|
}))
|
|
|
|
vi.mock(
|
|
'@/workbench/extensions/manager/composables/useConflictAcknowledgment',
|
|
() => {
|
|
const shouldShowConflictRedDot = ref(false)
|
|
mockData.setShowConflictRedDot = (value: boolean) => {
|
|
shouldShowConflictRedDot.value = value
|
|
}
|
|
|
|
return {
|
|
useConflictAcknowledgment: () => ({
|
|
shouldShowRedDot: shouldShowConflictRedDot
|
|
})
|
|
}
|
|
}
|
|
)
|
|
|
|
vi.mock('@/workbench/extensions/manager/composables/useManagerState', () => ({
|
|
useManagerState: () => ({
|
|
shouldShowManagerButtons: computed(() => true),
|
|
openManager: vi.fn()
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/stores/authStore', () => ({
|
|
useAuthStore: vi.fn(() => ({
|
|
currentUser: null,
|
|
loading: false
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/scripts/app', () => ({
|
|
app: {
|
|
menu: {
|
|
element: document.createElement('div')
|
|
}
|
|
}
|
|
}))
|
|
|
|
const mockTrackUiButtonClicked = vi.hoisted(() => vi.fn())
|
|
|
|
vi.mock('@/platform/telemetry', () => ({
|
|
useTelemetry: () => ({
|
|
trackUiButtonClicked: mockTrackUiButtonClicked
|
|
})
|
|
}))
|
|
|
|
type WrapperOptions = {
|
|
pinia?: ReturnType<typeof createTestingPinia>
|
|
stubs?: Record<string, boolean | Component>
|
|
attachTo?: HTMLElement
|
|
}
|
|
|
|
function createWrapper({
|
|
pinia = createTestingPinia({ createSpy: vi.fn }),
|
|
stubs = {},
|
|
attachTo
|
|
}: WrapperOptions = {}) {
|
|
const i18n = createI18n({
|
|
legacy: false,
|
|
locale: 'en',
|
|
messages: {
|
|
en: {
|
|
sideToolbar: {
|
|
queueProgressOverlay: {
|
|
viewJobHistory: 'View job history',
|
|
expandCollapsedQueue: 'Expand collapsed queue',
|
|
activeJobsShort: '{count} active | {count} active',
|
|
clearQueueTooltip: 'Clear queue'
|
|
}
|
|
},
|
|
rightSidePanel: {
|
|
togglePanel: 'Toggle properties panel'
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const user = userEvent.setup()
|
|
|
|
const renderOptions: Record<string, unknown> = {
|
|
global: {
|
|
plugins: [pinia, i18n],
|
|
stubs: {
|
|
SubgraphBreadcrumb: true,
|
|
QueueProgressOverlay: true,
|
|
QueueInlineProgressSummary: true,
|
|
QueueNotificationBannerHost: true,
|
|
CurrentUserButton: true,
|
|
LoginButton: true,
|
|
ContextMenu: {
|
|
name: 'ContextMenu',
|
|
props: ['model'],
|
|
template:
|
|
'<div data-testid="context-menu" :data-model="JSON.stringify(model)" />'
|
|
},
|
|
...stubs
|
|
},
|
|
directives: {
|
|
tooltip: () => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (attachTo) {
|
|
renderOptions.container = attachTo.appendChild(
|
|
document.createElement('div')
|
|
)
|
|
}
|
|
|
|
const { container, unmount } = render(TopMenuSection, renderOptions)
|
|
|
|
return { container, unmount, user }
|
|
}
|
|
|
|
function getLegacyCommandsContainer(container: Element): HTMLElement {
|
|
const legacyContainer = container.querySelector(
|
|
'[data-testid="legacy-topbar-container"]'
|
|
)
|
|
if (!(legacyContainer instanceof HTMLElement)) {
|
|
throw new Error('Expected legacy commands container to be present')
|
|
}
|
|
return legacyContainer
|
|
}
|
|
|
|
function createJob(id: string, status: JobStatus): JobListItem {
|
|
return {
|
|
id,
|
|
status,
|
|
create_time: 0,
|
|
priority: 0
|
|
}
|
|
}
|
|
|
|
function createTask(id: string, status: JobStatus): TaskItemImpl {
|
|
return new TaskItemImpl(createJob(id, status))
|
|
}
|
|
|
|
function createComfyActionbarStub(actionbarTarget: HTMLElement) {
|
|
return defineComponent({
|
|
name: 'ComfyActionbar',
|
|
setup(_, { emit }) {
|
|
onMounted(() => {
|
|
emit('update:progressTarget', actionbarTarget)
|
|
})
|
|
return () => h('div')
|
|
}
|
|
})
|
|
}
|
|
|
|
describe('TopMenuSection', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks()
|
|
localStorage.clear()
|
|
mockData.isDesktop = false
|
|
mockData.isLoggedIn = false
|
|
mockData.setShowConflictRedDot(false)
|
|
})
|
|
|
|
describe('authentication state', () => {
|
|
function createLegacyTabBarWrapper() {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
const settingStore = useSettingStore(pinia)
|
|
vi.mocked(settingStore.get).mockImplementation((key) =>
|
|
key === 'Comfy.UI.TabBarLayout' ? 'Legacy' : undefined
|
|
)
|
|
return createWrapper({ pinia })
|
|
}
|
|
|
|
describe('when user is logged in', () => {
|
|
beforeEach(() => {
|
|
mockData.isLoggedIn = true
|
|
})
|
|
|
|
it('should display CurrentUserButton and not display LoginButton', () => {
|
|
const { container } = createLegacyTabBarWrapper()
|
|
expect(
|
|
container.querySelector('current-user-button-stub')
|
|
).not.toBeNull()
|
|
expect(container.querySelector('login-button-stub')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('when user is not logged in', () => {
|
|
beforeEach(() => {
|
|
mockData.isLoggedIn = false
|
|
})
|
|
|
|
describe('on desktop platform', () => {
|
|
it('should display LoginButton and not display CurrentUserButton', () => {
|
|
mockData.isDesktop = true
|
|
const { container } = createLegacyTabBarWrapper()
|
|
expect(container.querySelector('login-button-stub')).not.toBeNull()
|
|
expect(container.querySelector('current-user-button-stub')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('on web platform', () => {
|
|
it('should not display CurrentUserButton and not display LoginButton', () => {
|
|
const { container } = createLegacyTabBarWrapper()
|
|
expect(container.querySelector('current-user-button-stub')).toBeNull()
|
|
expect(container.querySelector('login-button-stub')).toBeNull()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
it('shows the active jobs label with the current count', async () => {
|
|
createWrapper()
|
|
const queueStore = useQueueStore()
|
|
queueStore.pendingTasks = [createTask('pending-1', 'pending')]
|
|
queueStore.runningTasks = [
|
|
createTask('running-1', 'in_progress'),
|
|
createTask('running-2', 'in_progress')
|
|
]
|
|
|
|
await nextTick()
|
|
|
|
const queueButton = screen.getByTestId('queue-overlay-toggle')
|
|
expect(queueButton.textContent).toContain('3 active')
|
|
expect(screen.getByTestId('active-jobs-indicator')).toBeTruthy()
|
|
})
|
|
|
|
it('hides the active jobs indicator when no jobs are active', () => {
|
|
createWrapper()
|
|
|
|
expect(screen.queryByTestId('active-jobs-indicator')).toBeNull()
|
|
})
|
|
|
|
it('tracks right side panel opens', async () => {
|
|
const { user } = createWrapper()
|
|
|
|
await user.click(
|
|
screen.getByRole('button', { name: 'Toggle properties panel' })
|
|
)
|
|
|
|
expect(mockTrackUiButtonClicked).toHaveBeenCalledWith({
|
|
button_id: 'right_side_panel_opened',
|
|
element_group: 'top_menu'
|
|
})
|
|
})
|
|
|
|
it('hides queue progress overlay when QPO V2 is enabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
const settingStore = useSettingStore(pinia)
|
|
vi.mocked(settingStore.get).mockImplementation((key) =>
|
|
key === 'Comfy.Queue.QPOV2' ? true : undefined
|
|
)
|
|
const { container } = createWrapper({ pinia })
|
|
|
|
await nextTick()
|
|
|
|
expect(screen.getByTestId('queue-overlay-toggle')).toBeTruthy()
|
|
expect(container.querySelector('queue-progress-overlay-stub')).toBeNull()
|
|
})
|
|
|
|
it('toggles the queue progress overlay when QPO V2 is disabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false })
|
|
const settingStore = useSettingStore(pinia)
|
|
vi.mocked(settingStore.get).mockImplementation((key) =>
|
|
key === 'Comfy.Queue.QPOV2' ? false : undefined
|
|
)
|
|
const { user } = createWrapper({ pinia })
|
|
const commandStore = useCommandStore(pinia)
|
|
|
|
await user.click(screen.getByTestId('queue-overlay-toggle'))
|
|
|
|
expect(commandStore.execute).toHaveBeenCalledWith(
|
|
'Comfy.Queue.ToggleOverlay'
|
|
)
|
|
})
|
|
|
|
it('opens the job history sidebar tab when QPO V2 is enabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false })
|
|
const settingStore = useSettingStore(pinia)
|
|
vi.mocked(settingStore.get).mockImplementation((key) =>
|
|
key === 'Comfy.Queue.QPOV2' ? true : undefined
|
|
)
|
|
const { user } = createWrapper({ pinia })
|
|
const sidebarTabStore = useSidebarTabStore(pinia)
|
|
|
|
await user.click(screen.getByTestId('queue-overlay-toggle'))
|
|
|
|
expect(sidebarTabStore.activeSidebarTabId).toBe('job-history')
|
|
})
|
|
|
|
it('toggles the job history sidebar tab when QPO V2 is enabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false })
|
|
const settingStore = useSettingStore(pinia)
|
|
vi.mocked(settingStore.get).mockImplementation((key) =>
|
|
key === 'Comfy.Queue.QPOV2' ? true : undefined
|
|
)
|
|
const { user } = createWrapper({ pinia })
|
|
const sidebarTabStore = useSidebarTabStore(pinia)
|
|
const toggleButton = screen.getByTestId('queue-overlay-toggle')
|
|
|
|
await user.click(toggleButton)
|
|
expect(sidebarTabStore.activeSidebarTabId).toBe('job-history')
|
|
|
|
await user.click(toggleButton)
|
|
expect(sidebarTabStore.activeSidebarTabId).toBe(null)
|
|
})
|
|
|
|
describe('inline progress summary', () => {
|
|
const configureSettings = (
|
|
pinia: ReturnType<typeof createTestingPinia>,
|
|
qpoV2Enabled: boolean,
|
|
showRunProgressBar = true
|
|
) => {
|
|
const settingStore = useSettingStore(pinia)
|
|
vi.mocked(settingStore.get).mockImplementation((key) => {
|
|
if (key === 'Comfy.Queue.QPOV2') return qpoV2Enabled
|
|
if (key === 'Comfy.Queue.ShowRunProgressBar') return showRunProgressBar
|
|
if (key === 'Comfy.UseNewMenu') return 'Top'
|
|
return undefined
|
|
})
|
|
}
|
|
|
|
it('renders inline progress summary when QPO V2 is enabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
configureSettings(pinia, true)
|
|
|
|
const { container } = createWrapper({ pinia })
|
|
|
|
await nextTick()
|
|
|
|
expect(
|
|
container.querySelector('queue-inline-progress-summary-stub')
|
|
).not.toBeNull()
|
|
})
|
|
|
|
it('does not render inline progress summary when QPO V2 is disabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
configureSettings(pinia, false)
|
|
|
|
const { container } = createWrapper({ pinia })
|
|
|
|
await nextTick()
|
|
|
|
expect(
|
|
container.querySelector('queue-inline-progress-summary-stub')
|
|
).toBeNull()
|
|
})
|
|
|
|
it('does not render inline progress summary when run progress bar is disabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
configureSettings(pinia, true, false)
|
|
|
|
const { container } = createWrapper({ pinia })
|
|
|
|
await nextTick()
|
|
|
|
expect(
|
|
container.querySelector('queue-inline-progress-summary-stub')
|
|
).toBeNull()
|
|
})
|
|
|
|
it('teleports inline progress summary when actionbar is floating', async () => {
|
|
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
|
|
const actionbarTarget = document.createElement('div')
|
|
document.body.appendChild(actionbarTarget)
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
configureSettings(pinia, true)
|
|
const executionStore = useExecutionStore(pinia)
|
|
executionStore.activeJobId = 'job-1'
|
|
|
|
const ComfyActionbarStub = createComfyActionbarStub(actionbarTarget)
|
|
|
|
const { unmount } = createWrapper({
|
|
pinia,
|
|
attachTo: document.body,
|
|
stubs: {
|
|
ComfyActionbar: ComfyActionbarStub,
|
|
QueueInlineProgressSummary: false
|
|
}
|
|
})
|
|
|
|
try {
|
|
await nextTick()
|
|
|
|
expect(actionbarTarget.querySelector('[role="status"]')).not.toBeNull()
|
|
} finally {
|
|
unmount()
|
|
actionbarTarget.remove()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe(QueueNotificationBannerHost, () => {
|
|
const configureSettings = (
|
|
pinia: ReturnType<typeof createTestingPinia>,
|
|
qpoV2Enabled: boolean
|
|
) => {
|
|
const settingStore = useSettingStore(pinia)
|
|
vi.mocked(settingStore.get).mockImplementation((key) => {
|
|
if (key === 'Comfy.Queue.QPOV2') return qpoV2Enabled
|
|
if (key === 'Comfy.UseNewMenu') return 'Top'
|
|
return undefined
|
|
})
|
|
}
|
|
|
|
it('renders queue notification banners when QPO V2 is enabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
configureSettings(pinia, true)
|
|
|
|
const { container } = createWrapper({ pinia })
|
|
|
|
await nextTick()
|
|
|
|
expect(
|
|
container.querySelector('queue-notification-banner-host-stub')
|
|
).not.toBeNull()
|
|
})
|
|
|
|
it('renders queue notification banners when QPO V2 is disabled', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
configureSettings(pinia, false)
|
|
|
|
const { container } = createWrapper({ pinia })
|
|
|
|
await nextTick()
|
|
|
|
expect(
|
|
container.querySelector('queue-notification-banner-host-stub')
|
|
).not.toBeNull()
|
|
})
|
|
|
|
it('renders inline summary above banners when both are visible', async () => {
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
configureSettings(pinia, true)
|
|
const { container } = createWrapper({ pinia })
|
|
|
|
await nextTick()
|
|
|
|
const html = container.innerHTML
|
|
const inlineSummaryIndex = html.indexOf(
|
|
'queue-inline-progress-summary-stub'
|
|
)
|
|
const queueBannerIndex = html.indexOf(
|
|
'queue-notification-banner-host-stub'
|
|
)
|
|
|
|
expect(inlineSummaryIndex).toBeGreaterThan(-1)
|
|
expect(queueBannerIndex).toBeGreaterThan(-1)
|
|
expect(inlineSummaryIndex).toBeLessThan(queueBannerIndex)
|
|
})
|
|
|
|
it('does not teleport queue notification banners when actionbar is floating', async () => {
|
|
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
|
|
const actionbarTarget = document.createElement('div')
|
|
document.body.appendChild(actionbarTarget)
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
configureSettings(pinia, true)
|
|
const executionStore = useExecutionStore(pinia)
|
|
executionStore.activeJobId = 'job-1'
|
|
|
|
const ComfyActionbarStub = createComfyActionbarStub(actionbarTarget)
|
|
|
|
const { container, unmount } = createWrapper({
|
|
pinia,
|
|
attachTo: document.body,
|
|
stubs: {
|
|
ComfyActionbar: ComfyActionbarStub,
|
|
QueueNotificationBannerHost: true
|
|
}
|
|
})
|
|
|
|
try {
|
|
await nextTick()
|
|
|
|
expect(
|
|
actionbarTarget.querySelector('queue-notification-banner-host-stub')
|
|
).toBeNull()
|
|
expect(
|
|
container.querySelector('queue-notification-banner-host-stub')
|
|
).not.toBeNull()
|
|
} finally {
|
|
unmount()
|
|
actionbarTarget.remove()
|
|
}
|
|
})
|
|
})
|
|
|
|
it('disables the clear queue context menu item when no queued jobs exist', () => {
|
|
const { container } = createWrapper()
|
|
const menuEl = container.querySelector('[data-testid="context-menu"]')
|
|
const model = JSON.parse(
|
|
menuEl?.getAttribute('data-model') ?? '[]'
|
|
) as MenuItem[]
|
|
expect(model[0]?.label).toBe('Clear queue')
|
|
expect(model[0]?.disabled).toBe(true)
|
|
})
|
|
|
|
it('enables the clear queue context menu item when queued jobs exist', async () => {
|
|
const { container } = createWrapper()
|
|
const queueStore = useQueueStore()
|
|
queueStore.pendingTasks = [createTask('pending-1', 'pending')]
|
|
|
|
await nextTick()
|
|
|
|
const menuEl = container.querySelector('[data-testid="context-menu"]')
|
|
const model = JSON.parse(
|
|
menuEl?.getAttribute('data-model') ?? '[]'
|
|
) as MenuItem[]
|
|
expect(model[0]?.disabled).toBe(false)
|
|
})
|
|
|
|
it('shows manager red dot only for manager conflicts', async () => {
|
|
const { container } = createWrapper()
|
|
|
|
// Release red dot is mocked as true globally for this test file.
|
|
expect(container.querySelector('span.bg-red-500')).toBeNull()
|
|
|
|
mockData.setShowConflictRedDot(true)
|
|
await nextTick()
|
|
|
|
expect(container.querySelector('span.bg-red-500')).not.toBeNull()
|
|
})
|
|
|
|
it('coalesces legacy topbar mutation scans to one check per frame', async () => {
|
|
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
|
|
|
|
const rafCallbacks: FrameRequestCallback[] = []
|
|
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
|
rafCallbacks.push(cb)
|
|
return rafCallbacks.length
|
|
})
|
|
vi.stubGlobal('cancelAnimationFrame', vi.fn())
|
|
|
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
|
const settingStore = useSettingStore(pinia)
|
|
vi.mocked(settingStore.get).mockImplementation((key) => {
|
|
if (key === 'Comfy.UseNewMenu') return 'Top'
|
|
if (key === 'Comfy.UI.TabBarLayout') return 'Integrated'
|
|
if (key === 'Comfy.RightSidePanel.IsOpen') return true
|
|
return undefined
|
|
})
|
|
|
|
const { container, unmount } = createWrapper({
|
|
pinia,
|
|
attachTo: document.body
|
|
})
|
|
|
|
try {
|
|
await nextTick()
|
|
|
|
const actionbarContainer = container.querySelector('.actionbar-container')
|
|
expect(actionbarContainer).not.toBeNull()
|
|
expect(actionbarContainer!.classList).toContain('w-0')
|
|
|
|
const legacyContainer = getLegacyCommandsContainer(container)
|
|
const querySpy = vi.spyOn(legacyContainer, 'querySelector')
|
|
|
|
if (rafCallbacks.length > 0) {
|
|
const initialCallbacks = [...rafCallbacks]
|
|
rafCallbacks.length = 0
|
|
initialCallbacks.forEach((callback) => callback(0))
|
|
await nextTick()
|
|
}
|
|
querySpy.mockClear()
|
|
querySpy.mockReturnValue(document.createElement('div'))
|
|
|
|
for (let index = 0; index < 3; index++) {
|
|
const outer = document.createElement('div')
|
|
const inner = document.createElement('div')
|
|
inner.textContent = `legacy-${index}`
|
|
outer.appendChild(inner)
|
|
legacyContainer.appendChild(outer)
|
|
}
|
|
|
|
await vi.waitFor(() => {
|
|
expect(rafCallbacks.length).toBeGreaterThan(0)
|
|
})
|
|
expect(querySpy).not.toHaveBeenCalled()
|
|
|
|
const callbacks = [...rafCallbacks]
|
|
rafCallbacks.length = 0
|
|
callbacks.forEach((callback) => callback(0))
|
|
await nextTick()
|
|
|
|
expect(querySpy).toHaveBeenCalledTimes(1)
|
|
expect(actionbarContainer!.classList).toContain('px-2')
|
|
} finally {
|
|
unmount()
|
|
vi.unstubAllGlobals()
|
|
}
|
|
})
|
|
})
|