mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
fix: File Upload widget disabled prop treats undefined as true (#5528)
* fix file upload widget disabled prop
* [test] extract createMockWidget to shared test utility - addresses @DrJKL's code replication concern
Creates testUtils.ts with shared createMockWidget and createMockFile functions
to reduce duplication across widget component tests. This ensures consistency
and maintainability of test setup code.
* [test] replace type assertions with type narrowing - addresses @DrJKL's type safety suggestion
Replaces unsafe `as HTMLInputElement` casts with proper instanceof checks
and error throwing. Also refactors File Type Detection tests to use it.for
instead of conditionals to eliminate anti-pattern.
* [feat] use destructuring with default value for readonly prop - addresses @DrJKL's Vue best practice suggestion
Replace manual fallback expressions like `readonly || false` with modern Vue 3
destructuring pattern: `const { readonly = false } = defineProps()`.
This is cleaner than withDefaults() and follows current Vue best practices.
* [test] improve test utilities usage - addresses @DrJKL's additional suggestions
- Replace findComponent with getComponent for better error handling
- Use optional chaining (?.()) instead of conditional checks for cleaner syntax
- Remove unnecessary existence checks since getComponent throws on failure
This commit is contained in:
@@ -0,0 +1,588 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import PrimeVue from 'primevue/config'
|
||||||
|
import Select from 'primevue/select'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import enMessages from '@/locales/en/main.json'
|
||||||
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
|
import { createMockFile, createMockWidget } from '../testUtils'
|
||||||
|
import WidgetFileUpload from './WidgetFileUpload.vue'
|
||||||
|
|
||||||
|
describe('WidgetFileUpload File Handling', () => {
|
||||||
|
const mountComponent = (
|
||||||
|
widget: SimplifiedWidget<File[] | null>,
|
||||||
|
modelValue: File[] | null,
|
||||||
|
readonly = false
|
||||||
|
) => {
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
...enMessages,
|
||||||
|
'Drop your file or': 'Drop your file or'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return mount(WidgetFileUpload, {
|
||||||
|
global: {
|
||||||
|
plugins: [PrimeVue, i18n],
|
||||||
|
components: { Button, Select }
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
widget,
|
||||||
|
modelValue,
|
||||||
|
readonly
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockObjectURL = 'blob:mock-url'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock URL.createObjectURL and revokeObjectURL
|
||||||
|
global.URL.createObjectURL = vi.fn(() => mockObjectURL)
|
||||||
|
global.URL.revokeObjectURL = vi.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Initial States', () => {
|
||||||
|
it('shows upload UI when no file is selected', () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null)
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Drop your file or')
|
||||||
|
expect(wrapper.text()).toContain('Browse Files')
|
||||||
|
expect(wrapper.find('button').text()).toBe('Browse Files')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders file input with correct attributes', () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
null,
|
||||||
|
{ accept: 'image/*' },
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, null)
|
||||||
|
|
||||||
|
const fileInput = wrapper.find('input[type="file"]')
|
||||||
|
expect(fileInput.exists()).toBe(true)
|
||||||
|
expect(fileInput.attributes('accept')).toBe('image/*')
|
||||||
|
expect(fileInput.classes()).toContain('hidden')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('File Selection', () => {
|
||||||
|
it('triggers file input when browse button is clicked', async () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null)
|
||||||
|
|
||||||
|
const fileInput = wrapper.find('input[type="file"]')
|
||||||
|
const inputElement = fileInput.element
|
||||||
|
if (!(inputElement instanceof HTMLInputElement)) {
|
||||||
|
throw new Error('Expected HTMLInputElement')
|
||||||
|
}
|
||||||
|
const clickSpy = vi.spyOn(inputElement, 'click')
|
||||||
|
|
||||||
|
const browseButton = wrapper.find('button')
|
||||||
|
await browseButton.trigger('click')
|
||||||
|
|
||||||
|
expect(clickSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles file selection', async () => {
|
||||||
|
const mockCallback = vi.fn()
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, mockCallback, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null)
|
||||||
|
|
||||||
|
const file = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const fileInput = wrapper.find('input[type="file"]')
|
||||||
|
|
||||||
|
Object.defineProperty(fileInput.element, 'files', {
|
||||||
|
value: [file],
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
await fileInput.trigger('change')
|
||||||
|
|
||||||
|
const emitted = wrapper.emitted('update:modelValue')
|
||||||
|
expect(emitted).toBeDefined()
|
||||||
|
expect(emitted![0]).toEqual([[file]])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resets file input after selection', async () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null)
|
||||||
|
|
||||||
|
const file = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const fileInput = wrapper.find('input[type="file"]')
|
||||||
|
|
||||||
|
Object.defineProperty(fileInput.element, 'files', {
|
||||||
|
value: [file],
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
await fileInput.trigger('change')
|
||||||
|
|
||||||
|
const inputElement = fileInput.element
|
||||||
|
if (!(inputElement instanceof HTMLInputElement)) {
|
||||||
|
throw new Error('Expected HTMLInputElement')
|
||||||
|
}
|
||||||
|
expect(inputElement.value).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Image File Display', () => {
|
||||||
|
it('shows image preview for image files', () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile])
|
||||||
|
|
||||||
|
const img = wrapper.find('img')
|
||||||
|
expect(img.exists()).toBe(true)
|
||||||
|
expect(img.attributes('src')).toBe(mockObjectURL)
|
||||||
|
expect(img.attributes('alt')).toBe('test.jpg')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows select dropdown with filename for images', () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile])
|
||||||
|
|
||||||
|
const select = wrapper.getComponent({ name: 'Select' })
|
||||||
|
expect(select.props('modelValue')).toBe('test.jpg')
|
||||||
|
expect(select.props('options')).toEqual(['test.jpg'])
|
||||||
|
expect(select.props('disabled')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows edit and delete buttons on hover for images', () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile])
|
||||||
|
|
||||||
|
// The pi-pencil and pi-times classes are on the <i> elements inside the buttons
|
||||||
|
const editIcon = wrapper.find('i.pi-pencil')
|
||||||
|
const deleteIcon = wrapper.find('i.pi-times')
|
||||||
|
|
||||||
|
expect(editIcon.exists()).toBe(true)
|
||||||
|
expect(deleteIcon.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides control buttons in readonly mode', () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile], true)
|
||||||
|
|
||||||
|
const controlButtons = wrapper.find('.absolute.top-2.right-2')
|
||||||
|
expect(controlButtons.exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Audio File Display', () => {
|
||||||
|
it('shows audio player for audio files', () => {
|
||||||
|
const audioFile = createMockFile('test.mp3', 'audio/mpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[audioFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [audioFile])
|
||||||
|
|
||||||
|
expect(wrapper.find('.pi-volume-up').exists()).toBe(true)
|
||||||
|
expect(wrapper.text()).toContain('test.mp3')
|
||||||
|
expect(wrapper.text()).toContain('1.0 KB')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows file size for audio files', () => {
|
||||||
|
const audioFile = createMockFile('test.mp3', 'audio/mpeg', 2048)
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[audioFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [audioFile])
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('2.0 KB')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows delete button for audio files', () => {
|
||||||
|
const audioFile = createMockFile('test.mp3', 'audio/mpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[audioFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [audioFile])
|
||||||
|
|
||||||
|
const deleteIcon = wrapper.find('i.pi-times')
|
||||||
|
expect(deleteIcon.exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('File Type Detection', () => {
|
||||||
|
const imageFiles = [
|
||||||
|
{ name: 'image.jpg', type: 'image/jpeg' },
|
||||||
|
{ name: 'image.png', type: 'image/png' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const audioFiles = [
|
||||||
|
{ name: 'audio.mp3', type: 'audio/mpeg' },
|
||||||
|
{ name: 'audio.wav', type: 'audio/wav' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const normalFiles = [
|
||||||
|
{ name: 'video.mp4', type: 'video/mp4' },
|
||||||
|
{ name: 'document.pdf', type: 'application/pdf' }
|
||||||
|
]
|
||||||
|
|
||||||
|
it.for(imageFiles)(
|
||||||
|
'shows image preview for $type files',
|
||||||
|
({ name, type }) => {
|
||||||
|
const file = createMockFile(name, type)
|
||||||
|
const widget = createMockWidget<File[] | null>([file], {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, [file])
|
||||||
|
|
||||||
|
expect(wrapper.find('img').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('.pi-volume-up').exists()).toBe(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.for(audioFiles)(
|
||||||
|
'shows audio player for $type files',
|
||||||
|
({ name, type }) => {
|
||||||
|
const file = createMockFile(name, type)
|
||||||
|
const widget = createMockWidget<File[] | null>([file], {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, [file])
|
||||||
|
|
||||||
|
expect(wrapper.find('.pi-volume-up').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('img').exists()).toBe(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.for(normalFiles)('shows normal UI for $type files', ({ name, type }) => {
|
||||||
|
const file = createMockFile(name, type)
|
||||||
|
const widget = createMockWidget<File[] | null>([file], {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, [file])
|
||||||
|
|
||||||
|
expect(wrapper.find('img').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.pi-volume-up').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('File Actions', () => {
|
||||||
|
it('clears file when delete button is clicked', async () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile])
|
||||||
|
|
||||||
|
// Find button that contains the times icon
|
||||||
|
const buttons = wrapper.findAll('button')
|
||||||
|
const deleteButton = buttons.find((button) =>
|
||||||
|
button.find('i.pi-times').exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!deleteButton) {
|
||||||
|
throw new Error('Delete button with times icon not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteButton.trigger('click')
|
||||||
|
|
||||||
|
const emitted = wrapper.emitted('update:modelValue')
|
||||||
|
expect(emitted).toBeDefined()
|
||||||
|
expect(emitted![emitted!.length - 1]).toEqual([null])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles edit button click', async () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile])
|
||||||
|
|
||||||
|
// Find button that contains the pencil icon
|
||||||
|
const buttons = wrapper.findAll('button')
|
||||||
|
const editButton = buttons.find((button) =>
|
||||||
|
button.find('i.pi-pencil').exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!editButton) {
|
||||||
|
throw new Error('Edit button with pencil icon not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not throw error when clicked (TODO: implement edit functionality)
|
||||||
|
await expect(editButton.trigger('click')).resolves.not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('triggers file input when folder button is clicked', async () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile])
|
||||||
|
|
||||||
|
const fileInput = wrapper.find('input[type="file"]')
|
||||||
|
const inputElement = fileInput.element
|
||||||
|
if (!(inputElement instanceof HTMLInputElement)) {
|
||||||
|
throw new Error('Expected HTMLInputElement')
|
||||||
|
}
|
||||||
|
const clickSpy = vi.spyOn(inputElement, 'click')
|
||||||
|
|
||||||
|
// Find PrimeVue Button component with folder icon
|
||||||
|
const folderButton = wrapper.getComponent(Button)
|
||||||
|
|
||||||
|
await folderButton.trigger('click')
|
||||||
|
|
||||||
|
expect(clickSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Readonly Mode', () => {
|
||||||
|
it('disables browse button in readonly mode', () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null, true)
|
||||||
|
|
||||||
|
const browseButton = wrapper.find('button')
|
||||||
|
expect(browseButton.element.disabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables file input in readonly mode', () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null, true)
|
||||||
|
|
||||||
|
const fileInput = wrapper.find('input[type="file"]')
|
||||||
|
const inputElement = fileInput.element
|
||||||
|
if (!(inputElement instanceof HTMLInputElement)) {
|
||||||
|
throw new Error('Expected HTMLInputElement')
|
||||||
|
}
|
||||||
|
expect(inputElement.disabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disables folder button for images in readonly mode', () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile], true)
|
||||||
|
|
||||||
|
const buttons = wrapper.findAll('button')
|
||||||
|
const folderButton = buttons.find((button) =>
|
||||||
|
button.element.innerHTML.includes('pi-folder')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!folderButton) {
|
||||||
|
throw new Error('Folder button not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(folderButton.element.disabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not handle file changes in readonly mode', async () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null, true)
|
||||||
|
|
||||||
|
const file = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const fileInput = wrapper.find('input[type="file"]')
|
||||||
|
|
||||||
|
Object.defineProperty(fileInput.element, 'files', {
|
||||||
|
value: [file],
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
await fileInput.trigger('change')
|
||||||
|
|
||||||
|
const emitted = wrapper.emitted('update:modelValue')
|
||||||
|
expect(emitted).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('handles empty file selection gracefully', async () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null)
|
||||||
|
|
||||||
|
const fileInput = wrapper.find('input[type="file"]')
|
||||||
|
|
||||||
|
Object.defineProperty(fileInput.element, 'files', {
|
||||||
|
value: [],
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
await fileInput.trigger('change')
|
||||||
|
|
||||||
|
const emitted = wrapper.emitted('update:modelValue')
|
||||||
|
expect(emitted).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles missing file input gracefully', () => {
|
||||||
|
const widget = createMockWidget<File[] | null>(null, {}, undefined, {
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
const wrapper = mountComponent(widget, null)
|
||||||
|
|
||||||
|
// Remove file input ref to simulate missing element
|
||||||
|
wrapper.vm.$refs.fileInputRef = null
|
||||||
|
|
||||||
|
// Should not throw error when method exists
|
||||||
|
const vm = wrapper.vm as any
|
||||||
|
expect(() => vm.triggerFileInput?.()).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles clearing file when no file input exists', async () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile])
|
||||||
|
|
||||||
|
// Remove file input ref to simulate missing element
|
||||||
|
wrapper.vm.$refs.fileInputRef = null
|
||||||
|
|
||||||
|
// Find button that contains the times icon
|
||||||
|
const buttons = wrapper.findAll('button')
|
||||||
|
const deleteButton = buttons.find((button) =>
|
||||||
|
button.find('i.pi-times').exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!deleteButton) {
|
||||||
|
throw new Error('Delete button with times icon not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not throw error
|
||||||
|
await expect(deleteButton.trigger('click')).resolves.not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cleans up object URLs on unmount', () => {
|
||||||
|
const imageFile = createMockFile('test.jpg', 'image/jpeg')
|
||||||
|
const widget = createMockWidget<File[] | null>(
|
||||||
|
[imageFile],
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
name: 'test_file_upload',
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const wrapper = mountComponent(widget, [imageFile])
|
||||||
|
|
||||||
|
wrapper.unmount()
|
||||||
|
|
||||||
|
expect(global.URL.revokeObjectURL).toHaveBeenCalledWith(mockObjectURL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -187,7 +187,11 @@ import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
|||||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
const props = defineProps<{
|
const {
|
||||||
|
widget,
|
||||||
|
modelValue,
|
||||||
|
readonly = false
|
||||||
|
} = defineProps<{
|
||||||
widget: SimplifiedWidget<File[] | null>
|
widget: SimplifiedWidget<File[] | null>
|
||||||
modelValue: File[] | null
|
modelValue: File[] | null
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
@@ -198,8 +202,8 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { localValue, onChange } = useWidgetValue({
|
const { localValue, onChange } = useWidgetValue({
|
||||||
widget: props.widget,
|
widget,
|
||||||
modelValue: props.modelValue,
|
modelValue,
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
emit
|
emit
|
||||||
})
|
})
|
||||||
@@ -280,7 +284,7 @@ const triggerFileInput = () => {
|
|||||||
|
|
||||||
const handleFileChange = (event: Event) => {
|
const handleFileChange = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
if (!props.readonly && target.files && target.files.length > 0) {
|
if (!readonly && target.files && target.files.length > 0) {
|
||||||
// Since we only support single file, take the first one
|
// Since we only support single file, take the first one
|
||||||
const file = target.files[0]
|
const file = target.files[0]
|
||||||
|
|
||||||
|
|||||||
33
src/renderer/extensions/vueNodes/widgets/testUtils.ts
Normal file
33
src/renderer/extensions/vueNodes/widgets/testUtils.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock SimplifiedWidget for testing Vue Node widgets.
|
||||||
|
* This utility function is shared across widget component tests to ensure consistency.
|
||||||
|
*/
|
||||||
|
export function createMockWidget<T extends WidgetValue>(
|
||||||
|
value: T = null as T,
|
||||||
|
options: Record<string, any> = {},
|
||||||
|
callback?: (value: T) => void,
|
||||||
|
overrides: Partial<SimplifiedWidget<T>> = {}
|
||||||
|
): SimplifiedWidget<T> {
|
||||||
|
return {
|
||||||
|
name: 'test_widget',
|
||||||
|
type: 'default',
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
callback,
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock file for testing file upload widgets.
|
||||||
|
*/
|
||||||
|
export function createMockFile(name: string, type: string, size = 1024): File {
|
||||||
|
const file = new File(['mock content'], name, { type })
|
||||||
|
Object.defineProperty(file, 'size', {
|
||||||
|
value: size,
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
return file
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user