diff --git a/src/components/queue/job/JobContextMenu.test.ts b/src/components/queue/job/JobContextMenu.test.ts new file mode 100644 index 0000000000..57c248e70d --- /dev/null +++ b/src/components/queue/job/JobContextMenu.test.ts @@ -0,0 +1,81 @@ +import { mount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' + +import JobContextMenu from '@/components/queue/job/JobContextMenu.vue' +import type { MenuEntry } from '@/composables/queue/useJobMenu' + +const buttonStub = { + props: { + disabled: { + type: Boolean, + default: false + } + }, + template: ` +
+ +
+ ` +} + +const createEntries = (): MenuEntry[] => [ + { key: 'enabled', label: 'Enabled action', onClick: vi.fn() }, + { + key: 'disabled', + label: 'Disabled action', + disabled: true, + onClick: vi.fn() + }, + { kind: 'divider', key: 'divider-1' } +] + +const mountComponent = (entries: MenuEntry[]) => + mount(JobContextMenu, { + props: { entries }, + global: { + stubs: { + Popover: { + template: '
' + }, + Button: buttonStub + } + } + }) + +describe('JobContextMenu', () => { + it('passes disabled state to action buttons', () => { + const wrapper = mountComponent(createEntries()) + + const buttons = wrapper.findAll('.button-stub') + expect(buttons).toHaveLength(2) + expect(buttons[0].attributes('data-disabled')).toBe('false') + expect(buttons[1].attributes('data-disabled')).toBe('true') + }) + + it('emits action for enabled entries', async () => { + const entries = createEntries() + const wrapper = mountComponent(entries) + + await wrapper.findAll('.button-stub')[0].trigger('click') + + expect(wrapper.emitted('action')).toEqual([[entries[0]]]) + }) + + it('does not emit action for disabled entries', async () => { + const wrapper = mountComponent([ + { + key: 'disabled', + label: 'Disabled action', + disabled: true, + onClick: vi.fn() + } + ]) + + await wrapper.get('.button-stub').trigger('click') + + expect(wrapper.emitted('action')).toBeUndefined() + }) +}) diff --git a/src/components/queue/job/JobContextMenu.vue b/src/components/queue/job/JobContextMenu.vue index 5ed96ad9ec..c8c4e1bddf 100644 --- a/src/components/queue/job/JobContextMenu.vue +++ b/src/components/queue/job/JobContextMenu.vue @@ -26,6 +26,7 @@ variant="textonly" size="sm" :aria-label="entry.label" + :disabled="entry.disabled" @click="onEntry(entry)" > { 'd3', 'delete' ]) + + expect( + findActionEntry(jobMenuEntries.value, 'inspect-asset')?.disabled + ).toBe(false) + expect( + findActionEntry(jobMenuEntries.value, 'add-to-current')?.disabled + ).toBe(false) + expect(findActionEntry(jobMenuEntries.value, 'download')?.disabled).toBe( + false + ) + const inspectEntry = findActionEntry(jobMenuEntries.value, 'inspect-asset') await inspectEntry?.onClick?.() expect(inspectSpy).toHaveBeenCalledWith(currentItem.value) @@ -760,6 +771,7 @@ describe('useJobMenu', () => { await nextTick() const inspectEntry = findActionEntry(jobMenuEntries.value, 'inspect-asset') expect(inspectEntry?.onClick).toBeUndefined() + expect(inspectEntry?.disabled).toBe(true) }) it('omits delete asset entry when no preview exists', async () => { @@ -767,6 +779,15 @@ describe('useJobMenu', () => { setCurrentItem(createJobItem({ state: 'completed', taskRef: {} })) await nextTick() + expect( + findActionEntry(jobMenuEntries.value, 'inspect-asset')?.disabled + ).toBe(true) + expect( + findActionEntry(jobMenuEntries.value, 'add-to-current')?.disabled + ).toBe(true) + expect(findActionEntry(jobMenuEntries.value, 'download')?.disabled).toBe( + true + ) expect(jobMenuEntries.value.some((entry) => entry.key === 'delete')).toBe( false ) diff --git a/src/composables/queue/useJobMenu.ts b/src/composables/queue/useJobMenu.ts index 0dd9908538..7c573657ea 100644 --- a/src/composables/queue/useJobMenu.ts +++ b/src/composables/queue/useJobMenu.ts @@ -29,6 +29,7 @@ export type MenuEntry = key: string label: string icon?: string + disabled?: boolean onClick?: () => void | Promise } | { kind: 'divider'; key: string } @@ -239,13 +240,14 @@ export function useJobMenu( const item = currentMenuItem() const state = item?.state if (!state) return [] - const hasDeletableAsset = !!item?.taskRef?.previewOutput + const hasPreviewAsset = !!item?.taskRef?.previewOutput if (state === 'completed') { return [ { key: 'inspect-asset', label: st('queue.jobMenu.inspectAsset', 'Inspect asset'), icon: 'icon-[lucide--zoom-in]', + disabled: !hasPreviewAsset || !onInspectAsset, onClick: onInspectAsset ? () => { const item = currentMenuItem() @@ -260,12 +262,14 @@ export function useJobMenu( 'Add to current workflow' ), icon: 'icon-[comfy--node]', + disabled: !hasPreviewAsset, onClick: addOutputLoaderNode }, { key: 'download', label: st('queue.jobMenu.download', 'Download'), icon: 'icon-[lucide--download]', + disabled: !hasPreviewAsset, onClick: downloadPreviewAsset }, { kind: 'divider', key: 'd1' }, @@ -289,7 +293,7 @@ export function useJobMenu( onClick: copyJobId }, { kind: 'divider', key: 'd3' }, - ...(hasDeletableAsset + ...(hasPreviewAsset ? [ { key: 'delete',