diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts index 6d37d81079..ec0d5804bf 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts @@ -15,7 +15,34 @@ import type { SimplifiedWidget } from '@/types/simplifiedWidget' import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue' import { createMockWidget } from './widgetTestUtils' +const mockCheckState = vi.hoisted(() => vi.fn()) const mockAssetsData = vi.hoisted(() => ({ items: [] as AssetItem[] })) + +vi.mock('@/platform/workflow/management/stores/workflowStore', async () => { + const actual = await vi.importActual( + '@/platform/workflow/management/stores/workflowStore' + ) + return { + ...actual, + useWorkflowStore: () => ({ + activeWorkflow: { + changeTracker: { + checkState: mockCheckState + } + } + }) + } +}) + +vi.mock('@/scripts/api', () => ({ + api: { + fetchApi: vi.fn(), + apiURL: vi.fn((url: string) => url), + addEventListener: vi.fn(), + removeEventListener: vi.fn() + } +})) + vi.mock( '@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData', () => ({ @@ -456,3 +483,69 @@ describe('WidgetSelectDropdown cloud asset mode (COM-14333)', () => { expect(selectedSet.has('missing-missing_model.safetensors')).toBe(true) }) }) + +describe('WidgetSelectDropdown undo tracking', () => { + interface UndoTrackingInstance extends ComponentPublicInstance { + updateSelectedItems: (selectedSet: Set) => void + handleFilesUpdate: (files: File[]) => Promise + } + + const mountForUndo = ( + widget: SimplifiedWidget, + modelValue: string | undefined + ): VueWrapper => { + return mount(WidgetSelectDropdown, { + props: { + widget, + modelValue, + assetKind: 'image', + allowUpload: true, + uploadFolder: 'input' + }, + global: { + plugins: [PrimeVue, createTestingPinia(), i18n] + } + }) as unknown as VueWrapper + } + + beforeEach(() => { + mockCheckState.mockClear() + }) + + it('calls checkState after dropdown selection changes modelValue', () => { + const widget = createMockWidget({ + value: 'img_001.png', + name: 'test_image', + type: 'combo', + options: { values: ['img_001.png', 'photo_abc.jpg'] } + }) + const wrapper = mountForUndo(widget, 'img_001.png') + + wrapper.vm.updateSelectedItems(new Set(['input-1'])) + + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['photo_abc.jpg']) + expect(mockCheckState).toHaveBeenCalledOnce() + }) + + it('calls checkState after file upload completes', async () => { + const { api } = await import('@/scripts/api') + vi.mocked(api.fetchApi).mockResolvedValue({ + status: 200, + json: () => Promise.resolve({ name: 'uploaded.png', subfolder: '' }) + } as Response) + + const widget = createMockWidget({ + value: 'img_001.png', + name: 'test_image', + type: 'combo', + options: { values: ['img_001.png'] } + }) + const wrapper = mountForUndo(widget, 'img_001.png') + + const file = new File(['test'], 'uploaded.png', { type: 'image/png' }) + await wrapper.vm.handleFilesUpdate([file]) + + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['uploaded.png']) + expect(mockCheckState).toHaveBeenCalledOnce() + }) +}) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue index a4f81983f3..5930b0f37d 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue @@ -17,6 +17,7 @@ import { getAssetFilename } from '@/platform/assets/utils/assetMetadataUtils' import { useToastStore } from '@/platform/updates/common/toastStore' +import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import FormDropdown from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue' import type { FilterOption, @@ -376,6 +377,7 @@ function updateSelectedItems(selectedItems: Set) { return } modelValue.value = name + useWorkflowStore().activeWorkflow?.changeTracker?.checkState() } const uploadFile = async ( @@ -450,6 +452,9 @@ async function handleFilesUpdate(files: File[]) { if (props.widget.callback) { props.widget.callback(uploadedPaths[0]) } + + // 5. Snapshot undo state so the image change gets its own undo entry + useWorkflowStore().activeWorkflow?.changeTracker?.checkState() } catch (error) { console.error('Upload error:', error) toastStore.addAlert(`Upload failed: ${error}`)