From fcdb5ddfe6f526accfeb73ef25d4af127fc35148 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 11 Oct 2025 12:14:55 -0700 Subject: [PATCH 1/6] refactor and reorganize dev tool nodes --- tools/devtools/nodes/inputs.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tools/devtools/nodes/inputs.py b/tools/devtools/nodes/inputs.py index ac31056cad..0f65b8ca4c 100644 --- a/tools/devtools/nodes/inputs.py +++ b/tools/devtools/nodes/inputs.py @@ -280,29 +280,6 @@ def execute(self, int_input: int): return tuple() -class NodeWithV2ComboInput: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "combo_input": ( - "COMBO", - {"options": ["A", "B"]}, - ), - } - } - - RETURN_TYPES = ("COMBO",) - FUNCTION = "node_with_v2_combo_input" - CATEGORY = "DevTools" - DESCRIPTION = ( - "A node that outputs a combo type that adheres to the v2 combo input spec" - ) - - def node_with_v2_combo_input(self, combo_input: str): - return (combo_input,) - - NODE_CLASS_MAPPINGS = { "DevToolsLongComboDropdown": LongComboDropdown, "DevToolsNodeWithOptionalInput": NodeWithOptionalInput, @@ -317,7 +294,6 @@ def node_with_v2_combo_input(self, combo_input: str): "DevToolsSimpleSlider": SimpleSlider, "DevToolsNodeWithSeedInput": NodeWithSeedInput, "DevToolsNodeWithValidation": NodeWithValidation, - "DevToolsNodeWithV2ComboInput": NodeWithV2ComboInput, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -334,7 +310,6 @@ def node_with_v2_combo_input(self, combo_input: str): "DevToolsSimpleSlider": "Simple Slider", "DevToolsNodeWithSeedInput": "Node With Seed Input", "DevToolsNodeWithValidation": "Node With Validation", - "DevToolsNodeWithV2ComboInput": "Node With V2 Combo Input", } __all__ = [ @@ -351,7 +326,6 @@ def node_with_v2_combo_input(self, combo_input: str): "SimpleSlider", "NodeWithSeedInput", "NodeWithValidation", - "NodeWithV2ComboInput", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", ] From 998510136ad850049c453f56b8e7b74a2e7c9419 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 11 Oct 2025 13:07:10 -0700 Subject: [PATCH 2/6] add accidentally deleted node back --- tools/devtools/nodes/inputs.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tools/devtools/nodes/inputs.py b/tools/devtools/nodes/inputs.py index 0f65b8ca4c..ac31056cad 100644 --- a/tools/devtools/nodes/inputs.py +++ b/tools/devtools/nodes/inputs.py @@ -280,6 +280,29 @@ def execute(self, int_input: int): return tuple() +class NodeWithV2ComboInput: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "combo_input": ( + "COMBO", + {"options": ["A", "B"]}, + ), + } + } + + RETURN_TYPES = ("COMBO",) + FUNCTION = "node_with_v2_combo_input" + CATEGORY = "DevTools" + DESCRIPTION = ( + "A node that outputs a combo type that adheres to the v2 combo input spec" + ) + + def node_with_v2_combo_input(self, combo_input: str): + return (combo_input,) + + NODE_CLASS_MAPPINGS = { "DevToolsLongComboDropdown": LongComboDropdown, "DevToolsNodeWithOptionalInput": NodeWithOptionalInput, @@ -294,6 +317,7 @@ def execute(self, int_input: int): "DevToolsSimpleSlider": SimpleSlider, "DevToolsNodeWithSeedInput": NodeWithSeedInput, "DevToolsNodeWithValidation": NodeWithValidation, + "DevToolsNodeWithV2ComboInput": NodeWithV2ComboInput, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -310,6 +334,7 @@ def execute(self, int_input: int): "DevToolsSimpleSlider": "Simple Slider", "DevToolsNodeWithSeedInput": "Node With Seed Input", "DevToolsNodeWithValidation": "Node With Validation", + "DevToolsNodeWithV2ComboInput": "Node With V2 Combo Input", } __all__ = [ @@ -326,6 +351,7 @@ def execute(self, int_input: int): "SimpleSlider", "NodeWithSeedInput", "NodeWithValidation", + "NodeWithV2ComboInput", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", ] From 4699859de028f94b4a0dd186e676cc130d638e8e Mon Sep 17 00:00:00 2001 From: bymyself Date: Thu, 6 Nov 2025 13:31:27 -0700 Subject: [PATCH 3/6] refactor: improve Vue widget type safety and runtime prop handling - Add proper type guards for all widget input specs (Color, TreeSelect, MultiSelect, FileUpload, Galleria) - Enhance schemas with missing properties (format, placeholder, accept, extensions, tooltip) - Fix widgets to honor runtime props like disabled while accessing spec metadata - Eliminate all 'as any' usage in widget components with proper TypeScript types - Clean separation: widget.spec.options for metadata, widget.options for runtime state - Refactor devtools into modular structure with vue_widgets showcase nodes --- browser_tests/utils/devtoolsSync.ts | 41 ++ .../widgets/components/WidgetColorPicker.vue | 15 +- .../widgets/components/WidgetFileUpload.vue | 7 +- .../widgets/components/WidgetGalleria.vue | 21 +- .../widgets/components/WidgetMultiSelect.vue | 14 +- .../widgets/components/WidgetSelectButton.vue | 14 +- .../widgets/components/WidgetTreeSelect.vue | 39 +- src/schemas/nodeDef/nodeDefSchemaV2.ts | 54 +- tools/devtools/README.md | 4 +- tools/devtools/dev_nodes.py | 24 + tools/devtools/nodes/__init__.py | 30 ++ tools/devtools/nodes/vue_widgets.py | 477 ++++++++++++++++++ 12 files changed, 699 insertions(+), 41 deletions(-) create mode 100644 browser_tests/utils/devtoolsSync.ts create mode 100644 tools/devtools/nodes/vue_widgets.py diff --git a/browser_tests/utils/devtoolsSync.ts b/browser_tests/utils/devtoolsSync.ts new file mode 100644 index 0000000000..594effe7da --- /dev/null +++ b/browser_tests/utils/devtoolsSync.ts @@ -0,0 +1,41 @@ +import fs from 'fs-extra' +import path from 'path' +import { fileURLToPath } from 'url' + +export function syncDevtools(targetComfyDir: string) { + if (!targetComfyDir) { + console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set') + return + } + + const moduleDir = + typeof __dirname !== 'undefined' + ? __dirname + : path.dirname(fileURLToPath(import.meta.url)) + + const devtoolsSrc = path.resolve(moduleDir, '..', '..', 'tools', 'devtools') + + if (!fs.pathExistsSync(devtoolsSrc)) { + console.warn( + `syncDevtools skipped: source directory not found at ${devtoolsSrc}` + ) + return + } + + const devtoolsDest = path.resolve( + targetComfyDir, + 'custom_nodes', + 'ComfyUI_devtools' + ) + + console.warn(`syncDevtools: copying ${devtoolsSrc} -> ${devtoolsDest}`) + + try { + fs.removeSync(devtoolsDest) + fs.ensureDirSync(devtoolsDest) + fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true }) + console.warn('syncDevtools: copy complete') + } catch (error) { + console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error) + } +} diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue index 333966996c..85396754c6 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.vue @@ -27,6 +27,7 @@ import ColorPicker from 'primevue/colorpicker' import { computed, ref, watch } from 'vue' +import { isColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import type { SimplifiedWidget } from '@/types/simplifiedWidget' import { isColorFormat, toHexFromFormat } from '@/utils/colorUtil' import type { ColorFormat, HSB } from '@/utils/colorUtil' @@ -51,18 +52,18 @@ const emit = defineEmits<{ }>() const format = computed(() => { - const optionFormat = props.widget.options?.format + const spec = props.widget.spec + if (!spec || !isColorInputSpec(spec)) { + return 'hex' + } + + const optionFormat = spec.options?.format return isColorFormat(optionFormat) ? optionFormat : 'hex' }) type PickerValue = string | HSB const localValue = ref( - toHexFromFormat( - props.modelValue || '#000000', - isColorFormat(props.widget.options?.format) - ? props.widget.options.format - : 'hex' - ) + toHexFromFormat(props.modelValue || '#000000', format.value) ) watch( diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue index 912bfb2f93..a210bd4739 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.vue @@ -174,7 +174,11 @@ ref="fileInputRef" type="file" class="hidden" - :accept="widget.options?.accept" + :accept=" + widget.spec && isFileUploadInputSpec(widget.spec) + ? widget.spec.options?.accept + : undefined + " :aria-label="`${$t('g.upload')} ${widget.name || $t('g.file')}`" :multiple="false" @change="handleFileChange" @@ -188,6 +192,7 @@ import { computed, onUnmounted, ref, watch } from 'vue' import { useWidgetValue } from '@/composables/graph/useWidgetValue' import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps' +import { isFileUploadInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import type { SimplifiedWidget } from '@/types/simplifiedWidget' const { widget, modelValue } = defineProps<{ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue index e32acb7817..23809431b5 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue @@ -53,6 +53,7 @@ import Galleria from 'primevue/galleria' import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' +import { isGalleriaInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import type { SimplifiedWidget } from '@/types/simplifiedWidget' import { GALLERIA_EXCLUDED_PROPS, @@ -78,9 +79,9 @@ const activeIndex = ref(0) const { t } = useI18n() -const filteredProps = computed(() => - filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS) -) +const filteredProps = computed(() => { + return filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS) +}) const galleryImages = computed(() => { if (!value.value || !Array.isArray(value.value)) return [] @@ -100,16 +101,22 @@ const galleryImages = computed(() => { }) const showThumbnails = computed(() => { + const spec = props.widget.spec + if (!spec || !isGalleriaInputSpec(spec)) { + return galleryImages.value.length > 1 + } return ( - props.widget.options?.showThumbnails !== false && - galleryImages.value.length > 1 + spec.options?.showThumbnails !== false && galleryImages.value.length > 1 ) }) const showNavButtons = computed(() => { + const spec = props.widget.spec + if (!spec || !isGalleriaInputSpec(spec)) { + return galleryImages.value.length > 1 + } return ( - props.widget.options?.showItemNavigators !== false && - galleryImages.value.length > 1 + spec.options?.showItemNavigators !== false && galleryImages.value.length > 1 ) }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue index 167d45c2ca..ef01a89e0e 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.vue @@ -23,6 +23,7 @@ import { computed } from 'vue' import { useWidgetValue } from '@/composables/graph/useWidgetValue' import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps' +import { isMultiSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget' import { PANEL_EXCLUDED_PROPS, @@ -57,19 +58,20 @@ const MULTISELECT_EXCLUDED_PROPS = [ 'overlayStyle' ] as const +// Extract spec options directly const combinedProps = computed(() => ({ ...filterWidgetProps(props.widget.options, MULTISELECT_EXCLUDED_PROPS), ...transformCompatProps.value })) -// Extract multiselect options from widget options +// Extract multiselect options from widget spec options const multiSelectOptions = computed((): T[] => { - const options = props.widget.options - - if (Array.isArray(options?.values)) { - return options.values + const spec = props.widget.spec + if (!spec || !isMultiSelectInputSpec(spec)) { + return [] } - return [] + const values = spec.options?.values + return Array.isArray(values) ? (values as T[]) : [] }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue index 9a5663957c..bed7d246ae 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue @@ -2,7 +2,7 @@ @@ -10,7 +10,10 @@ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue index fae52b5302..f64a4c2585 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue @@ -20,11 +20,8 @@ import { computed } from 'vue' import { useWidgetValue } from '@/composables/graph/useWidgetValue' import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps' +import { isTreeSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import type { SimplifiedWidget } from '@/types/simplifiedWidget' -import { - PANEL_EXCLUDED_PROPS, - filterWidgetProps -} from '@/utils/widgetPropFilter' import WidgetLayoutField from './layout/WidgetLayoutField.vue' @@ -57,15 +54,29 @@ const { localValue, onChange } = useWidgetValue({ // Transform compatibility props for overlay positioning const transformCompatProps = useTransformCompatOverlayProps() -// TreeSelect specific excluded props -const TREE_SELECT_EXCLUDED_PROPS = [ - ...PANEL_EXCLUDED_PROPS, - 'inputClass', - 'inputStyle' -] as const +const combinedProps = computed(() => { + const spec = props.widget.spec + if (!spec || !isTreeSelectInputSpec(spec)) { + return { + ...props.widget.options, + ...transformCompatProps.value + } + } -const combinedProps = computed(() => ({ - ...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS), - ...transformCompatProps.value -})) + const specOptions = spec.options || {} + return { + // Include runtime props like disabled + ...props.widget.options, + // PrimeVue TreeSelect expects 'options' to be an array of tree nodes + options: (specOptions.values as TreeNode[]) || [], + // Convert 'multiple' to PrimeVue's 'selectionMode' + selectionMode: (specOptions.multiple ? 'multiple' : 'single') as + | 'single' + | 'multiple' + | 'checkbox', + // Pass through other props like placeholder + placeholder: specOptions.placeholder, + ...transformCompatProps.value + } +}) diff --git a/src/schemas/nodeDef/nodeDefSchemaV2.ts b/src/schemas/nodeDef/nodeDefSchemaV2.ts index 09983d115e..4ffd540fc9 100644 --- a/src/schemas/nodeDef/nodeDefSchemaV2.ts +++ b/src/schemas/nodeDef/nodeDefSchemaV2.ts @@ -45,7 +45,8 @@ const zColorInputSpec = zBaseInputOptions.extend({ isOptional: z.boolean().optional(), options: z .object({ - default: z.string().optional() + default: z.string().optional(), + format: z.enum(['hex', 'rgb', 'hsl', 'hsb']).optional() }) .optional() }) @@ -54,7 +55,13 @@ const zFileUploadInputSpec = zBaseInputOptions.extend({ type: z.literal('FILEUPLOAD'), name: z.string(), isOptional: z.boolean().optional(), - options: z.record(z.unknown()).optional() + options: z + .object({ + accept: z.string().optional(), + extensions: z.array(z.string()).optional(), + tooltip: z.string().optional() + }) + .optional() }) const zImageInputSpec = zBaseInputOptions.extend({ @@ -89,7 +96,8 @@ const zTreeSelectInputSpec = zBaseInputOptions.extend({ options: z .object({ multiple: z.boolean().optional(), - values: z.array(z.unknown()).optional() + values: z.array(z.unknown()).optional(), + placeholder: z.string().optional() }) .optional() }) @@ -123,7 +131,9 @@ const zGalleriaInputSpec = zBaseInputOptions.extend({ isOptional: z.boolean().optional(), options: z .object({ - images: z.array(z.string()).optional() + images: z.array(z.string()).optional(), + showThumbnails: z.boolean().optional(), + showItemNavigators: z.boolean().optional() }) .optional() }) @@ -262,3 +272,39 @@ export const isChartInputSpec = ( ): inputSpec is ChartInputSpec => { return inputSpec.type === 'CHART' } + +export const isTreeSelectInputSpec = ( + inputSpec: InputSpec +): inputSpec is TreeSelectInputSpec => { + return inputSpec.type === 'TREESELECT' +} + +export const isSelectButtonInputSpec = ( + inputSpec: InputSpec +): inputSpec is SelectButtonInputSpec => { + return inputSpec.type === 'SELECTBUTTON' +} + +export const isMultiSelectInputSpec = ( + inputSpec: InputSpec +): inputSpec is MultiSelectInputSpec => { + return inputSpec.type === 'MULTISELECT' +} + +export const isGalleriaInputSpec = ( + inputSpec: InputSpec +): inputSpec is GalleriaInputSpec => { + return inputSpec.type === 'GALLERIA' +} + +export const isColorInputSpec = ( + inputSpec: InputSpec +): inputSpec is ColorInputSpec => { + return inputSpec.type === 'COLOR' +} + +export const isFileUploadInputSpec = ( + inputSpec: InputSpec +): inputSpec is FileUploadInputSpec => { + return inputSpec.type === 'FILEUPLOAD' +} diff --git a/tools/devtools/README.md b/tools/devtools/README.md index d0d316ce81..dd6da43b5e 100644 --- a/tools/devtools/README.md +++ b/tools/devtools/README.md @@ -6,11 +6,13 @@ This directory contains development tools and test utilities for ComfyUI, previo - `__init__.py` - Server endpoints for development tools (`/api/devtools/*`) - `dev_nodes.py` - Development and testing nodes for ComfyUI +- `nodes/vue_widgets.py` - Widget showcase nodes used to exercise new Vue-based widgets - `fake_model.safetensors` - Test fixture for model loading tests ## Purpose These tools provide: + - Test endpoints for browser automation - Development nodes for testing various UI features - Mock data for consistent testing environments @@ -25,4 +27,4 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/ ## Migration -This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow. \ No newline at end of file +This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow. diff --git a/tools/devtools/dev_nodes.py b/tools/devtools/dev_nodes.py index 660518d846..c8bf860a1e 100644 --- a/tools/devtools/dev_nodes.py +++ b/tools/devtools/dev_nodes.py @@ -1,13 +1,20 @@ from __future__ import annotations from .nodes import ( + VueAudioPreviewComboNode, + VueAudioRecordWidgetNode, + VueChartWidgetNode, DeprecatedNode, DummyPatch, ErrorRaiseNode, ErrorRaiseNodeWithMessage, ExperimentalNode, + VueFileUploadWidgetNode, LoadAnimatedImageTest, LongComboDropdown, + VueMarkdownWidgetNode, + VueGalleriaWidgetNode, + VueImageCompareWidgetNode, MultiSelectNode, NodeWithBooleanInput, NodeWithDefaultInput, @@ -23,24 +30,36 @@ NodeWithValidation, NodeWithV2ComboInput, ObjectPatchNode, + VueSelectButtonWidgetNode, + VueTextareaWidgetNode, + VueTreeSelectMultiWidgetNode, + VueTreeSelectWidgetNode, RemoteWidgetNode, RemoteWidgetNodeWithControlAfterRefresh, RemoteWidgetNodeWithParams, RemoteWidgetNodeWithRefresh, RemoteWidgetNodeWithRefreshButton, SimpleSlider, + VueColorWidgetNode, NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, ) __all__ = [ + "VueAudioPreviewComboNode", + "VueAudioRecordWidgetNode", + "VueChartWidgetNode", "DeprecatedNode", "DummyPatch", "ErrorRaiseNode", "ErrorRaiseNodeWithMessage", "ExperimentalNode", + "VueFileUploadWidgetNode", "LoadAnimatedImageTest", "LongComboDropdown", + "VueMarkdownWidgetNode", + "VueGalleriaWidgetNode", + "VueImageCompareWidgetNode", "MultiSelectNode", "NodeWithBooleanInput", "NodeWithDefaultInput", @@ -56,12 +75,17 @@ "NodeWithValidation", "NodeWithV2ComboInput", "ObjectPatchNode", + "VueSelectButtonWidgetNode", + "VueTextareaWidgetNode", + "VueTreeSelectMultiWidgetNode", + "VueTreeSelectWidgetNode", "RemoteWidgetNode", "RemoteWidgetNodeWithControlAfterRefresh", "RemoteWidgetNodeWithParams", "RemoteWidgetNodeWithRefresh", "RemoteWidgetNodeWithRefreshButton", "SimpleSlider", + "VueColorWidgetNode", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", ] diff --git a/tools/devtools/nodes/__init__.py b/tools/devtools/nodes/__init__.py index f0ac2d8ee8..3be4f5f991 100644 --- a/tools/devtools/nodes/__init__.py +++ b/tools/devtools/nodes/__init__.py @@ -44,12 +44,29 @@ NODE_CLASS_MAPPINGS as remote_class_mappings, NODE_DISPLAY_NAME_MAPPINGS as remote_display_name_mappings, ) +from .vue_widgets import ( + VueAudioPreviewComboNode, + VueAudioRecordWidgetNode, + VueChartWidgetNode, + VueColorWidgetNode, + VueFileUploadWidgetNode, + VueGalleriaWidgetNode, + VueImageCompareWidgetNode, + VueMarkdownWidgetNode, + VueSelectButtonWidgetNode, + VueTextareaWidgetNode, + VueTreeSelectMultiWidgetNode, + VueTreeSelectWidgetNode, + NODE_CLASS_MAPPINGS as vue_widgets_class_mappings, + NODE_DISPLAY_NAME_MAPPINGS as vue_widgets_display_name_mappings, +) NODE_CLASS_MAPPINGS = { **errors_class_mappings, **inputs_class_mappings, **remote_class_mappings, **models_class_mappings, + **vue_widgets_class_mappings, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -57,6 +74,7 @@ **inputs_display_name_mappings, **remote_display_name_mappings, **models_display_name_mappings, + **vue_widgets_display_name_mappings, } __all__ = [ @@ -88,6 +106,18 @@ "RemoteWidgetNodeWithRefresh", "RemoteWidgetNodeWithRefreshButton", "SimpleSlider", + "VueAudioPreviewComboNode", + "VueAudioRecordWidgetNode", + "VueChartWidgetNode", + "VueColorWidgetNode", + "VueFileUploadWidgetNode", + "VueGalleriaWidgetNode", + "VueImageCompareWidgetNode", + "VueMarkdownWidgetNode", + "VueSelectButtonWidgetNode", + "VueTextareaWidgetNode", + "VueTreeSelectMultiWidgetNode", + "VueTreeSelectWidgetNode", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", ] diff --git a/tools/devtools/nodes/vue_widgets.py b/tools/devtools/nodes/vue_widgets.py new file mode 100644 index 0000000000..16fc48edb8 --- /dev/null +++ b/tools/devtools/nodes/vue_widgets.py @@ -0,0 +1,477 @@ +from __future__ import annotations + +SAMPLE_IMAGE_DATA_URI = ( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKklEQVR4nO3NsQEAAATAMLzr/5kn2NIDmpzu+Kxe7wAAAAAAAAAAAOCwBcUDAhU8Tp3xAAAAAElFTkSuQmCC" +) +SAMPLE_IMAGE_DATA_URI_ALT = ( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAALUlEQVR4nGM0nvWBgZaAiaamj1owasGoBaMWjFowasGoBaMWjFowasGoBVQEAKDTAf3D6Eg+AAAAAElFTkSuQmCC" +) +SAMPLE_IMAGE_DATA_URI_THIRD = ( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAALUlEQVR4nGMMPJ/GQEvARFPTRy0YtWDUglELRi0YtWDUglELRi0YtWDUAioCAAbrAcZ7cQHKAAAAAElFTkSuQmCC" +) + + +class VueFileUploadWidgetNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "uploaded_file": ( + "FILEUPLOAD", + { + "default": [], + "options": { + "extensions": ["png", "jpg", "jpeg", "webp"], + "accept": "image/png,image/jpeg,image/webp", + "tooltip": "Upload an image file", + }, + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "return_file" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the FILEUPLOAD widget" + + def return_file(self, uploaded_file: str | None): + return (uploaded_file or "",) + + +class VueImageCompareWidgetNode: + BEFORE_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAB4CAIAAAA48Cq8AAAC8ElEQVR4nO3Wv0uqURzH8Y/ykNXgFBQ1NERBNIS7jTUF4VBQ7RE4SH9Hg0jQ3KA1WVO74RYWREskREE0NRUNYvrc4XZBL3V/wP3kNd+v7eGcL5zhzTlPJEynBfxr0U4fAF8TYcGCsGBBWLAgLFgQFiwICxaEBQvCggVhwYKwYEFYsCAsWBAWLAgLFoQFC8KCBWHBgrBgQViwICxYEBYsCAsWhAULwoIFYcGCsGBBWLAgLFgQFiwICxaEBQvCggVhwYKwYEFYsCAsWBAWLAgLFoQFC8KCBWHBgrBgQViwICxYEBYsuhHW6KgWFzU01IWj8b8EEf9ePq/7e8ViGhzU6amurz/Zs7WlalVPNbMdHsJdEHdbzu07aaY0Pb252Elk6pUIj4XPSbqsH55elKzqeFhra4qmVQQ6PhYDw9aWFAioVxOpZLW1pRIKAxVLOrlRbu7urrS46POz3+fQr+xfWPNzKhcViajszPt7engQBsbklStKgxVKGh5WTVa8nnVaspiLCtCh8e6m0N7NZpclMUyo3LUqk0U0Ppra+vwOaPNZCn1ddn8dEtYvVSpOYqSRMT89g4MdKRpJKpRMoqXTdfqfBWm08RF4z/y9Ktfm+fXpLLjEgcFPTd1WsHyJRKxBqVTpIqH9f1f3xBd5JJkEplI+LtMv4ucVaFZ7o2j/AuAxn4OwIEFYkCAsSBAWJAgLEoQFCcKCBGFBgrAgQViQICxIEBYkCAsShAUJwoIEYUGCsCBBWJAgLEgQFiQICxKEBQnCggRhQYKwIEFYkCAsSBAWJAgLEoQFCcKCBGFBgrAgQViQICxIEBYkCAsShAUJwoIEYUGCsCBBWJAgLEgQFiw+AcwZf7lBBNZAAA" + AFTER_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAB4CAIAAAA48Cq8AAADDklEQVR4nO3Z32uNcRzA8e/3Oc85Oz/WLFqNzjZuxKgtWpmwluxiUYoLudlKubH/QHJBuZE7LuYfoCQXolxJasmF1EjJhRrhQpQ1O85zjouzEm69d7Ler6vvU8+Pz8W7b9964u7ZL0H615J2D6C1ybCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIRY1bA2dydTQx2dhbiaH1VbsGEd3VaYO7VufWmlpMuHKstZM2uE6eEO9LtqOzasA/3pjfnlfX351uWGcrwxX1uqN6eHDGuNA8MqprGYxjuvavsG0hDC8cFCJR9nD1dmRorlfLw6WenqiBfGy9cmK9ePVHb05FpPPZzqOj9WOrGzwA2mVZByrx6tpnML9bdfG5s6k3wSbr2szYwUT99dDCEcHyycubd47kDp5ovl+U9Zb2dyZaJ88va3EEIhFx+8+TG3UOcG0yoAwxobSLduyB3cku8pJ7s2pk/e/dnKaDXt61rZMkv5mMTQaIas2fz7Tv13qLCSGPrX5Vqb0Gg13d+f/zuXXIwz9xdrWUhiGE5NG80QQsgaobXQf406Yw33pq8/Z631sw/ZnupvBScxJDE8/1gf35wPIeztSz3OrzHUjjU2kD59v7JFfa83Py81tnT/ivjZh+zKROXS46Wz+0vHtheyZrj4aAmaRG0Rd89+afcMWoP8pSOEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQvwE4Ex5XANtu7QAAAAASUVORK5CYII=" + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "comparison": ( + "IMAGECOMPARE", + { + "default": { + "before": BEFORE_IMAGE, + "after": AFTER_IMAGE, + }, + "options": { + "beforeAlt": "Before", + "afterAlt": "After", + "initialPosition": 40, + }, + }, + ) + } + } + + RETURN_TYPES = () + FUNCTION = "noop" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the IMAGECOMPARE widget" + + def noop(self, comparison): + return tuple() + + +class VueTreeSelectWidgetNode: + TREE_DATA = [ + { + "key": "root", + "label": "Root", + "children": [ + { + "key": "section-a", + "label": "Section A", + "children": [ + {"key": "item-a1", "label": "Item A1"}, + {"key": "item-a2", "label": "Item A2"}, + ], + }, + { + "key": "section-b", + "label": "Section B", + "children": [ + {"key": "item-b1", "label": "Item B1"}, + {"key": "item-b2", "label": "Item B2"}, + ], + }, + ], + } + ] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "selection": ( + "TREESELECT", + { + "default": "item-a1", + "options": { + "values": cls.TREE_DATA, + "multiple": False, + "placeholder": "Select an item", + }, + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "return_selection" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the TREESELECT widget" + + def return_selection(self, selection: str): + return (selection,) + + +class VueTreeSelectMultiWidgetNode(VueTreeSelectWidgetNode): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "selection": ( + "TREESELECT", + { + "default": ["item-a1", "item-b1"], + "options": { + "values": cls.TREE_DATA, + "multiple": True, + "placeholder": "Select items", + }, + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + OUTPUT_IS_LIST = (True,) + FUNCTION = "return_selection" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the TREESELECT widget in multi-select mode" + + def return_selection(self, selection: list[str]): + return (selection,) + + +class VueSelectButtonWidgetNode: + @classmethod + def INPUT_TYPES(cls): + options = [ + {"label": "Low", "value": "low"}, + {"label": "Medium", "value": "medium"}, + {"label": "High", "value": "high"}, + ] + + return { + "required": { + "mode": ( + "SELECTBUTTON", + { + "default": "Medium", + "options": { + "values": ["Low", "Medium", "High"], + }, + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "return_mode" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the SELECTBUTTON widget" + + def return_mode(self, mode: str): + return (mode,) + + +class VueTextareaWidgetNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "notes": ( + "TEXTAREA", + { + "default": "This is a DevTools textarea widget.\nFeel free to edit me!", + "options": { + "rows": 4, + "cols": 40, + }, + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "return_notes" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the TEXTAREA widget" + + def return_notes(self, notes: str): + return (notes,) + + +class VueChartWidgetNode: + CHART_DATA = { + "labels": ["Iteration 1", "Iteration 2", "Iteration 3"], + "datasets": [ + { + "label": "Loss", + "data": [0.95, 0.62, 0.31], + "borderColor": "#339AF0", + "backgroundColor": "rgba(51, 154, 240, 0.2)", + "fill": True, + "tension": 0.35, + } + ], + } + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "chart": ( + "CHART", + { + "options": { + "type": "line", + "data": cls.CHART_DATA, + }, + }, + ) + } + } + + RETURN_TYPES = ("DICT",) + FUNCTION = "return_chart" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the CHART widget" + + def return_chart(self, chart): + return (chart,) + + +class VueGalleriaWidgetNode: + GALLERIA_IMAGES = [ + { + "itemImageSrc": SAMPLE_IMAGE_DATA_URI, + "thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI, + "alt": "Warm gradient", + }, + { + "itemImageSrc": SAMPLE_IMAGE_DATA_URI_ALT, + "thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI_ALT, + "alt": "Cool gradient", + }, + { + "itemImageSrc": SAMPLE_IMAGE_DATA_URI_THIRD, + "thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI_THIRD, + "alt": "Fresh gradient", + }, + ] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "gallery": ( + "GALLERIA", + { + "default": cls.GALLERIA_IMAGES, + "options": { + "images": cls.GALLERIA_IMAGES, + "showThumbnails": True, + "showItemNavigators": True, + }, + }, + ) + } + } + + RETURN_TYPES = () + FUNCTION = "noop" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the GALLERIA widget" + + def noop(self, gallery): + return tuple() + + +class VueMarkdownWidgetNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "markdown": ( + "MARKDOWN", + { + "default": "# DevTools Markdown\nThis widget renders **Markdown** content.", + "options": { + "content": "# DevTools Markdown\nThis widget renders **Markdown** content.", + }, + }, + ) + } + } + + RETURN_TYPES = () + FUNCTION = "noop" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the MARKDOWN widget" + + def noop(self, markdown): + return tuple() + + +class VueAudioRecordWidgetNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "recording": ( + "AUDIORECORD", + { + "default": "", + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "return_recording" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the AUDIORECORD widget" + + def return_recording(self, recording: str): + return (recording,) + + +class VueMultiSelectWidgetNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "selection": ( + "MULTISELECT", + { + "default": ["option1", "option3"], + "options": { + "values": ["option1", "option2", "option3", "option4", "option5"], + }, + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + OUTPUT_IS_LIST = (True,) + FUNCTION = "return_selection" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the MULTISELECT widget" + + def return_selection(self, selection: list[str]): + return (selection,) + + +class VueColorWidgetNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "color": ( + "COLOR", + { + "default": "#ff6b6b", + "options": { + "tooltip": "Pick a color", + }, + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "return_color" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the COLOR widget" + + def return_color(self, color: str): + return (color,) + + +class VueAudioPreviewComboNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "audio": ( + "COMBO", + { + "options": ["ambient.wav", "dialog.wav"], + "default": "ambient.wav", + "tooltip": "Pick an audio clip", + }, + ) + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "return_audio" + CATEGORY = "DevTools/Vue Widgets" + DESCRIPTION = "Showcases the COMBO widget rendered as Audio UI" + + def return_audio(self, audio: str): + return (audio,) + + +NODE_CLASS_MAPPINGS = { + "DevToolsVueFileUploadWidgetNode": VueFileUploadWidgetNode, + "DevToolsVueImageCompareWidgetNode": VueImageCompareWidgetNode, + "DevToolsVueTreeSelectWidgetNode": VueTreeSelectWidgetNode, + "DevToolsVueTreeSelectMultiWidgetNode": VueTreeSelectMultiWidgetNode, + "DevToolsVueSelectButtonWidgetNode": VueSelectButtonWidgetNode, + "DevToolsVueMultiSelectWidgetNode": VueMultiSelectWidgetNode, + "DevToolsVueTextareaWidgetNode": VueTextareaWidgetNode, + "DevToolsVueChartWidgetNode": VueChartWidgetNode, + "DevToolsVueGalleriaWidgetNode": VueGalleriaWidgetNode, + "DevToolsVueMarkdownWidgetNode": VueMarkdownWidgetNode, + "DevToolsVueAudioRecordWidgetNode": VueAudioRecordWidgetNode, + "DevToolsVueColorWidgetNode": VueColorWidgetNode, + "DevToolsVueAudioPreviewComboNode": VueAudioPreviewComboNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DevToolsVueFileUploadWidgetNode": "Vue File Upload Widget", + "DevToolsVueImageCompareWidgetNode": "Vue Image Compare Widget", + "DevToolsVueTreeSelectWidgetNode": "Vue Tree Select Widget", + "DevToolsVueTreeSelectMultiWidgetNode": "Vue Tree Select (Multi) Widget", + "DevToolsVueSelectButtonWidgetNode": "Vue Select Button Widget", + "DevToolsVueMultiSelectWidgetNode": "Vue Multi Select Widget", + "DevToolsVueTextareaWidgetNode": "Vue Textarea Widget", + "DevToolsVueChartWidgetNode": "Vue Chart Widget", + "DevToolsVueGalleriaWidgetNode": "Vue Galleria Widget", + "DevToolsVueMarkdownWidgetNode": "Vue Markdown Widget", + "DevToolsVueAudioRecordWidgetNode": "Vue Audio Record Widget", + "DevToolsVueColorWidgetNode": "Vue Color Widget", + "DevToolsVueAudioPreviewComboNode": "Vue Audio Combo Widget", +} + +__all__ = [ + "VueFileUploadWidgetNode", + "VueImageCompareWidgetNode", + "VueTreeSelectWidgetNode", + "VueTreeSelectMultiWidgetNode", + "VueSelectButtonWidgetNode", + "VueMultiSelectWidgetNode", + "VueTextareaWidgetNode", + "VueChartWidgetNode", + "VueGalleriaWidgetNode", + "VueMarkdownWidgetNode", + "VueAudioRecordWidgetNode", + "VueColorWidgetNode", + "VueAudioPreviewComboNode", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +] From 6596aa847e9cb42b581c6963563aff396a4acd68 Mon Sep 17 00:00:00 2001 From: bymyself Date: Thu, 6 Nov 2025 15:03:58 -0700 Subject: [PATCH 4/6] add sync devtools to globalsetup --- browser_tests/globalSetup.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts index 881ef11c43..da84489423 100644 --- a/browser_tests/globalSetup.ts +++ b/browser_tests/globalSetup.ts @@ -1,17 +1,20 @@ import type { FullConfig } from '@playwright/test' -import dotenv from 'dotenv' +import { config as loadEnv } from 'dotenv' import { backupPath } from './utils/backupUtils' +import { syncDevtools } from './utils/devtoolsSync' -dotenv.config() +loadEnv() -export default function globalSetup(config: FullConfig) { +export default function globalSetup(_: FullConfig) { if (!process.env.CI) { if (process.env.TEST_COMFYUI_DIR) { backupPath([process.env.TEST_COMFYUI_DIR, 'user']) backupPath([process.env.TEST_COMFYUI_DIR, 'models'], { renameAndReplaceWithScaffolding: true }) + + syncDevtools(process.env.TEST_COMFYUI_DIR) } else { console.warn( 'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten' From 5b33d3ff78a2c6395232f905110003abbb7715c1 Mon Sep 17 00:00:00 2001 From: bymyself Date: Thu, 13 Nov 2025 18:25:10 -0800 Subject: [PATCH 5/6] fix tests --- browser_tests/utils/devtoolsSync.ts | 19 +- .../components/WidgetColorPicker.test.ts | 73 ++-- .../components/WidgetFileUpload.test.ts | 9 +- .../widgets/components/WidgetGalleria.test.ts | 86 ++-- .../widgets/components/WidgetGalleria.vue | 22 +- .../components/WidgetMultiSelect.test.ts | 165 ++++++-- .../components/WidgetSelectButton.test.ts | 380 ++++++++++++++++-- .../widgets/components/WidgetSelectButton.vue | 2 +- .../widgets/components/WidgetTreeSelect.vue | 24 +- .../extensions/vueNodes/widgets/testUtils.ts | 15 +- src/schemas/nodeDef/nodeDefSchemaV2.ts | 7 + 11 files changed, 660 insertions(+), 142 deletions(-) diff --git a/browser_tests/utils/devtoolsSync.ts b/browser_tests/utils/devtoolsSync.ts index 594effe7da..4e9549c95b 100644 --- a/browser_tests/utils/devtoolsSync.ts +++ b/browser_tests/utils/devtoolsSync.ts @@ -2,10 +2,19 @@ import fs from 'fs-extra' import path from 'path' import { fileURLToPath } from 'url' -export function syncDevtools(targetComfyDir: string) { +export function syncDevtools(targetComfyDir: string): boolean { if (!targetComfyDir) { console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set') - return + return false + } + + // Validate and sanitize the target directory path + const resolvedTargetDir = path.resolve(targetComfyDir) + + // Basic path validation to prevent directory traversal + if (resolvedTargetDir.includes('..') || !path.isAbsolute(resolvedTargetDir)) { + console.error('syncDevtools failed: Invalid target directory path') + return false } const moduleDir = @@ -19,11 +28,11 @@ export function syncDevtools(targetComfyDir: string) { console.warn( `syncDevtools skipped: source directory not found at ${devtoolsSrc}` ) - return + return false } const devtoolsDest = path.resolve( - targetComfyDir, + resolvedTargetDir, 'custom_nodes', 'ComfyUI_devtools' ) @@ -35,7 +44,9 @@ export function syncDevtools(targetComfyDir: string) { fs.ensureDirSync(devtoolsDest) fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true }) console.warn('syncDevtools: copy complete') + return true } catch (error) { console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error) + return false } } diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.test.ts index 32a937f89a..fcd6f1fdac 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.test.ts @@ -5,12 +5,13 @@ import PrimeVue from 'primevue/config' import { describe, expect, it } from 'vitest' import type { SimplifiedWidget } from '@/types/simplifiedWidget' +import { createMockWidget } from '../testUtils' import WidgetColorPicker from './WidgetColorPicker.vue' import WidgetLayoutField from './layout/WidgetLayoutField.vue' describe('WidgetColorPicker Value Binding', () => { - const createMockWidget = ( + const createLocalMockWidget = ( value: string = '#000000', options: Partial = {}, callback?: (value: string) => void @@ -54,7 +55,7 @@ describe('WidgetColorPicker Value Binding', () => { describe('Vue Event Emission', () => { it('emits Vue event when color changes', async () => { - const widget = createMockWidget('#ff0000') + const widget = createLocalMockWidget('#ff0000') const wrapper = mountComponent(widget, '#ff0000') const emitted = await setColorPickerValue(wrapper, '#00ff00') @@ -64,7 +65,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('handles different color formats', async () => { - const widget = createMockWidget('#ffffff') + const widget = createLocalMockWidget('#ffffff') const wrapper = mountComponent(widget, '#ffffff') const emitted = await setColorPickerValue(wrapper, '#123abc') @@ -74,7 +75,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('handles missing callback gracefully', async () => { - const widget = createMockWidget('#000000', {}, undefined) + const widget = createLocalMockWidget('#000000', {}, undefined) const wrapper = mountComponent(widget, '#000000') const emitted = await setColorPickerValue(wrapper, '#ff00ff') @@ -85,7 +86,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('normalizes bare hex without # to #hex on emit', async () => { - const widget = createMockWidget('ff0000') + const widget = createLocalMockWidget('ff0000') const wrapper = mountComponent(widget, 'ff0000') const emitted = await setColorPickerValue(wrapper, '00ff00') @@ -95,7 +96,7 @@ describe('WidgetColorPicker Value Binding', () => { it('normalizes rgb() strings to #hex on emit', async (context) => { context.skip('needs diagnosis') - const widget = createMockWidget('#000000') + const widget = createLocalMockWidget('#000000') const wrapper = mountComponent(widget, '#000000') const emitted = await setColorPickerValue(wrapper, 'rgb(255, 0, 0)') @@ -104,7 +105,20 @@ describe('WidgetColorPicker Value Binding', () => { }) it('normalizes hsb() strings to #hex on emit', async () => { - const widget = createMockWidget('#000000', { format: 'hsb' }) + const widget = createMockWidget( + '#000000', + {}, + undefined, + { + name: 'test_color', + type: 'color' + }, + { + type: 'COLOR', + name: 'test_color', + options: { format: 'hsb' } + } + ) const wrapper = mountComponent(widget, '#000000') const emitted = await setColorPickerValue(wrapper, 'hsb(120, 100, 100)') @@ -113,7 +127,20 @@ describe('WidgetColorPicker Value Binding', () => { }) it('normalizes HSB object values to #hex on emit', async () => { - const widget = createMockWidget('#000000', { format: 'hsb' }) + const widget = createMockWidget( + '#000000', + {}, + undefined, + { + name: 'test_color', + type: 'color' + }, + { + type: 'COLOR', + name: 'test_color', + options: { format: 'hsb' } + } + ) const wrapper = mountComponent(widget, '#000000') const emitted = await setColorPickerValue(wrapper, { @@ -128,7 +155,7 @@ describe('WidgetColorPicker Value Binding', () => { describe('Component Rendering', () => { it('renders color picker component', () => { - const widget = createMockWidget('#ff0000') + const widget = createLocalMockWidget('#ff0000') const wrapper = mountComponent(widget, '#ff0000') const colorPicker = wrapper.findComponent({ name: 'ColorPicker' }) @@ -137,20 +164,20 @@ describe('WidgetColorPicker Value Binding', () => { it('normalizes display to a single leading #', () => { // Case 1: model value already includes '#' - let widget = createMockWidget('#ff0000') + let widget = createLocalMockWidget('#ff0000') let wrapper = mountComponent(widget, '#ff0000') let colorText = wrapper.find('[data-testid="widget-color-text"]') expect.soft(colorText.text()).toBe('#ff0000') // Case 2: model value missing '#' - widget = createMockWidget('ff0000') + widget = createLocalMockWidget('ff0000') wrapper = mountComponent(widget, 'ff0000') colorText = wrapper.find('[data-testid="widget-color-text"]') expect.soft(colorText.text()).toBe('#ff0000') }) it('renders layout field wrapper', () => { - const widget = createMockWidget('#ff0000') + const widget = createLocalMockWidget('#ff0000') const wrapper = mountComponent(widget, '#ff0000') const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' }) @@ -158,7 +185,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('displays current color value as text', () => { - const widget = createMockWidget('#ff0000') + const widget = createLocalMockWidget('#ff0000') const wrapper = mountComponent(widget, '#ff0000') const colorText = wrapper.find('[data-testid="widget-color-text"]') @@ -166,7 +193,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('updates color text when value changes', async () => { - const widget = createMockWidget('#ff0000') + const widget = createLocalMockWidget('#ff0000') const wrapper = mountComponent(widget, '#ff0000') await setColorPickerValue(wrapper, '#00ff00') @@ -178,7 +205,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('uses default color when no value provided', () => { - const widget = createMockWidget('') + const widget = createLocalMockWidget('') const wrapper = mountComponent(widget, '') const colorPicker = wrapper.findComponent({ name: 'ColorPicker' }) @@ -199,7 +226,7 @@ describe('WidgetColorPicker Value Binding', () => { ] for (const color of validHexColors) { - const widget = createMockWidget(color) + const widget = createLocalMockWidget(color) const wrapper = mountComponent(widget, color) const colorText = wrapper.find('[data-testid="widget-color-text"]') @@ -208,7 +235,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('handles short hex colors', () => { - const widget = createMockWidget('#fff') + const widget = createLocalMockWidget('#fff') const wrapper = mountComponent(widget, '#fff') const colorText = wrapper.find('[data-testid="widget-color-text"]') @@ -220,7 +247,7 @@ describe('WidgetColorPicker Value Binding', () => { format: 'hex' as const, inline: true } - const widget = createMockWidget('#ff0000', colorOptions) + const widget = createLocalMockWidget('#ff0000', colorOptions) const wrapper = mountComponent(widget, '#ff0000') const colorPicker = wrapper.findComponent({ name: 'ColorPicker' }) @@ -231,7 +258,7 @@ describe('WidgetColorPicker Value Binding', () => { describe('Widget Layout Integration', () => { it('passes widget to layout field', () => { - const widget = createMockWidget('#ff0000') + const widget = createLocalMockWidget('#ff0000') const wrapper = mountComponent(widget, '#ff0000') const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' }) @@ -239,7 +266,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('maintains proper component structure', () => { - const widget = createMockWidget('#ff0000') + const widget = createLocalMockWidget('#ff0000') const wrapper = mountComponent(widget, '#ff0000') // Should have layout field containing label with color picker and text @@ -257,7 +284,7 @@ describe('WidgetColorPicker Value Binding', () => { describe('Edge Cases', () => { it('handles empty color value', () => { - const widget = createMockWidget('') + const widget = createLocalMockWidget('') const wrapper = mountComponent(widget, '') const colorPicker = wrapper.findComponent({ name: 'ColorPicker' }) @@ -265,7 +292,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('handles invalid color formats gracefully', async () => { - const widget = createMockWidget('invalid-color') + const widget = createLocalMockWidget('invalid-color') const wrapper = mountComponent(widget, 'invalid-color') const colorText = wrapper.find('[data-testid="widget-color-text"]') @@ -277,7 +304,7 @@ describe('WidgetColorPicker Value Binding', () => { }) it('handles widget with no options', () => { - const widget = createMockWidget('#ff0000') + const widget = createLocalMockWidget('#ff0000') const wrapper = mountComponent(widget, '#ff0000') const colorPicker = wrapper.findComponent({ name: 'ColorPicker' }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.test.ts index 8453488152..fccfa779da 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetFileUpload.test.ts @@ -65,11 +65,18 @@ describe('WidgetFileUpload File Handling', () => { it('renders file input with correct attributes', () => { const widget = createMockWidget( null, - { accept: 'image/*' }, + {}, undefined, { name: 'test_file_upload', type: 'file' + }, + { + type: 'FILEUPLOAD', + name: 'test_file_upload', + options: { + accept: 'image/*' + } } ) const wrapper = mountComponent(widget, null) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.test.ts index 399edb5144..5f5336819f 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest' import { createI18n } from 'vue-i18n' import type { SimplifiedWidget } from '@/types/simplifiedWidget' +import { createMockWidget } from '../testUtils' import WidgetGalleria from './WidgetGalleria.vue' import type { GalleryImage, GalleryValue } from './WidgetGalleria.vue' @@ -45,7 +46,7 @@ const TEST_IMAGE_OBJECTS: readonly GalleryImage[] = Object.freeze([ ]) // Helper functions outside describe blocks for better clarity -function createMockWidget( +function createLocalMockWidget( value: GalleryValue = [], options: Partial = {} ): SimplifiedWidget { @@ -85,7 +86,20 @@ function createGalleriaWrapper( images: GalleryValue, options: Partial = {} ) { - const widget = createMockWidget(images, options) + const widget = createMockWidget( + images, + {}, + undefined, + { + name: 'test_galleria', + type: 'array' + }, + { + type: 'GALLERIA', + name: 'test_galleria', + options: options + } + ) return mountComponent(widget, images) } @@ -101,7 +115,7 @@ describe('WidgetGalleria Image Display', () => { }) it('displays empty gallery when no images provided', () => { - const widget = createMockWidget([]) + const widget = createLocalMockWidget([]) const wrapper = mountComponent(widget, []) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -109,7 +123,7 @@ describe('WidgetGalleria Image Display', () => { }) it('handles null or undefined value gracefully', () => { - const widget = createMockWidget([]) + const widget = createLocalMockWidget([]) const wrapper = mountComponent(widget, []) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -119,7 +133,7 @@ describe('WidgetGalleria Image Display', () => { describe('String Array Input', () => { it('converts string array to image objects', () => { - const widget = createMockWidget([...TEST_IMAGES_SMALL]) + const widget = createLocalMockWidget([...TEST_IMAGES_SMALL]) const wrapper = mountComponent(widget, [...TEST_IMAGES_SMALL]) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -134,7 +148,7 @@ describe('WidgetGalleria Image Display', () => { }) it('handles single string image', () => { - const widget = createMockWidget([...TEST_IMAGES_SINGLE]) + const widget = createLocalMockWidget([...TEST_IMAGES_SINGLE]) const wrapper = mountComponent(widget, [...TEST_IMAGES_SINGLE]) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -151,7 +165,7 @@ describe('WidgetGalleria Image Display', () => { describe('Object Array Input', () => { it('preserves image objects as-is', () => { - const widget = createMockWidget([...TEST_IMAGE_OBJECTS]) + const widget = createLocalMockWidget([...TEST_IMAGE_OBJECTS]) const wrapper = mountComponent(widget, [...TEST_IMAGE_OBJECTS]) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -166,7 +180,7 @@ describe('WidgetGalleria Image Display', () => { { itemImageSrc: 'https://example.com/image2.jpg' }, { thumbnailImageSrc: 'https://example.com/thumb3.jpg' } ] - const widget = createMockWidget(images) + const widget = createLocalMockWidget(images) const wrapper = mountComponent(widget, images) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -227,7 +241,20 @@ describe('WidgetGalleria Image Display', () => { it('respects widget option to hide navigation buttons', () => { const images = createImageStrings(3) - const widget = createMockWidget(images, { showItemNavigators: false }) + const widget = createMockWidget( + images, + {}, + undefined, + { + name: 'test_galleria', + type: 'array' + }, + { + type: 'GALLERIA', + name: 'test_galleria', + options: { showItemNavigators: false } + } + ) const wrapper = mountComponent(widget, images) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -236,7 +263,20 @@ describe('WidgetGalleria Image Display', () => { it('shows navigation buttons when explicitly enabled for multiple images', () => { const images = createImageStrings(3) - const widget = createMockWidget(images, { showItemNavigators: true }) + const widget = createMockWidget( + images, + {}, + undefined, + { + name: 'test_galleria', + type: 'array' + }, + { + type: 'GALLERIA', + name: 'test_galleria', + options: { showItemNavigators: true } + } + ) const wrapper = mountComponent(widget, images) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -247,7 +287,7 @@ describe('WidgetGalleria Image Display', () => { describe('Widget Options Handling', () => { it('passes through valid widget options', () => { const images = createImageStrings(2) - const widget = createMockWidget(images, { + const widget = createLocalMockWidget(images, { circular: true, autoPlay: true, transitionInterval: 3000 @@ -262,7 +302,7 @@ describe('WidgetGalleria Image Display', () => { it('applies custom styling props', () => { const images = createImageStrings(2) - const widget = createMockWidget(images) + const widget = createLocalMockWidget(images) const wrapper = mountComponent(widget, images) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -274,7 +314,7 @@ describe('WidgetGalleria Image Display', () => { describe('Active Index Management', () => { it('initializes with zero active index', () => { const images = createImageStrings(3) - const widget = createMockWidget(images) + const widget = createLocalMockWidget(images) const wrapper = mountComponent(widget, images) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -283,7 +323,7 @@ describe('WidgetGalleria Image Display', () => { it('can update active index', async () => { const images = createImageStrings(3) - const widget = createMockWidget(images) + const widget = createLocalMockWidget(images) const wrapper = mountComponent(widget, images) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -304,7 +344,7 @@ describe('WidgetGalleria Image Display', () => { }, { src: 'https://example.com/only-src.jpg' } ] - const widget = createMockWidget(images) + const widget = createLocalMockWidget(images) const wrapper = mountComponent(widget, images) // The template logic should prioritize itemImageSrc > src > fallback to the item itself @@ -320,7 +360,7 @@ describe('WidgetGalleria Image Display', () => { }, { src: 'https://example.com/only-src.jpg' } ] - const widget = createMockWidget(images) + const widget = createLocalMockWidget(images) const wrapper = mountComponent(widget, images) // The template logic should prioritize thumbnailImageSrc > src > fallback to the item itself @@ -331,7 +371,7 @@ describe('WidgetGalleria Image Display', () => { describe('Edge Cases', () => { it('handles empty array gracefully', () => { - const widget = createMockWidget([]) + const widget = createLocalMockWidget([]) const wrapper = mountComponent(widget, []) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -347,7 +387,7 @@ describe('WidgetGalleria Image Display', () => { null, // Null value undefined // Undefined value ] - const widget = createMockWidget(malformedImages as string[]) + const widget = createLocalMockWidget(malformedImages as string[]) const wrapper = mountComponent(widget, malformedImages as string[]) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -358,7 +398,7 @@ describe('WidgetGalleria Image Display', () => { it('handles very large image arrays', () => { const largeImageArray = createImageStrings(100) - const widget = createMockWidget(largeImageArray) + const widget = createLocalMockWidget(largeImageArray) const wrapper = mountComponent(widget, largeImageArray) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -374,7 +414,7 @@ describe('WidgetGalleria Image Display', () => { { itemImageSrc: 'https://example.com/object.jpg' }, 'https://example.com/another-string.jpg' ] - const widget = createMockWidget(mixedArray as string[]) + const widget = createLocalMockWidget(mixedArray as string[]) // The component expects consistent typing, but let's test it handles mixed input expect(() => mountComponent(widget, mixedArray as string[])).not.toThrow() @@ -382,7 +422,7 @@ describe('WidgetGalleria Image Display', () => { it('handles invalid URL strings', () => { const invalidUrls = ['not-a-url', '', ' ', 'http://', 'ftp://invalid'] - const widget = createMockWidget(invalidUrls) + const widget = createLocalMockWidget(invalidUrls) const wrapper = mountComponent(widget, invalidUrls) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -393,7 +433,7 @@ describe('WidgetGalleria Image Display', () => { describe('Styling and Layout', () => { it('applies max-width constraint', () => { const images = createImageStrings(2) - const widget = createMockWidget(images) + const widget = createLocalMockWidget(images) const wrapper = mountComponent(widget, images) const galleria = wrapper.findComponent({ name: 'Galleria' }) @@ -403,7 +443,7 @@ describe('WidgetGalleria Image Display', () => { it('applies passthrough props for thumbnails', () => { const images = createImageStrings(3) - const widget = createMockWidget(images) + const widget = createLocalMockWidget(images) const wrapper = mountComponent(widget, images) const galleria = wrapper.findComponent({ name: 'Galleria' }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue index 23809431b5..4ab14a316d 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue @@ -105,9 +105,14 @@ const showThumbnails = computed(() => { if (!spec || !isGalleriaInputSpec(spec)) { return galleryImages.value.length > 1 } - return ( - spec.options?.showThumbnails !== false && galleryImages.value.length > 1 - ) + + // If explicitly set to false, respect that regardless of image count + if (spec.options?.showThumbnails === false) { + return false + } + + // Otherwise show thumbnails if multiple images (or if explicitly set to true) + return galleryImages.value.length > 1 }) const showNavButtons = computed(() => { @@ -115,9 +120,14 @@ const showNavButtons = computed(() => { if (!spec || !isGalleriaInputSpec(spec)) { return galleryImages.value.length > 1 } - return ( - spec.options?.showItemNavigators !== false && galleryImages.value.length > 1 - ) + + // If explicitly set to false, respect that regardless of image count + if (spec.options?.showItemNavigators === false) { + return false + } + + // Otherwise show navigation buttons if multiple images (or if explicitly set to true) + return galleryImages.value.length > 1 }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.test.ts index f0fbff926a..1c9d4f0556 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMultiSelect.test.ts @@ -5,11 +5,12 @@ import type { MultiSelectProps } from 'primevue/multiselect' import { describe, expect, it } from 'vitest' import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget' +import { createMockWidget } from '../testUtils' import WidgetMultiSelect from './WidgetMultiSelect.vue' describe('WidgetMultiSelect Value Binding', () => { - const createMockWidget = ( + const createLocalMockWidget = ( value: WidgetValue[] = [], options: Partial & { values?: WidgetValue[] } = {}, callback?: (value: WidgetValue[]) => void @@ -50,9 +51,17 @@ describe('WidgetMultiSelect Value Binding', () => { describe('Vue Event Emission', () => { it('emits Vue event when selection changes', async () => { - const widget = createMockWidget([], { - values: ['option1', 'option2', 'option3'] - }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: ['option1', 'option2', 'option3'] } + } + ) const wrapper = mountComponent(widget, []) await setMultiSelectValueAndEmit(wrapper, ['option1', 'option2']) @@ -63,9 +72,17 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('emits Vue event when selection is cleared', async () => { - const widget = createMockWidget(['option1'], { - values: ['option1', 'option2'] - }) + const widget = createMockWidget( + ['option1'], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: ['option1', 'option2'] } + } + ) const wrapper = mountComponent(widget, ['option1']) await setMultiSelectValueAndEmit(wrapper, []) @@ -76,7 +93,7 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('handles single item selection', async () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: ['single'] }) const wrapper = mountComponent(widget, []) @@ -89,7 +106,7 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('emits update:modelValue for callback handling at parent level', async () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: ['option1', 'option2'] }) const wrapper = mountComponent(widget, []) @@ -103,7 +120,7 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('handles missing callback gracefully', async () => { - const widget = createMockWidget( + const widget = createLocalMockWidget( [], { values: ['option1'] @@ -123,7 +140,7 @@ describe('WidgetMultiSelect Value Binding', () => { describe('Component Rendering', () => { it('renders multiselect component', () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: ['option1', 'option2'] }) const wrapper = mountComponent(widget, []) @@ -134,7 +151,17 @@ describe('WidgetMultiSelect Value Binding', () => { it('displays options from widget values', () => { const options = ['apple', 'banana', 'cherry'] - const widget = createMockWidget([], { values: options }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: options } + } + ) const wrapper = mountComponent(widget, []) const multiselect = wrapper.findComponent({ name: 'MultiSelect' }) @@ -142,9 +169,17 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('displays initial selected values', () => { - const widget = createMockWidget(['banana'], { - values: ['apple', 'banana', 'cherry'] - }) + const widget = createMockWidget( + ['banana'], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: ['apple', 'banana', 'cherry'] } + } + ) const wrapper = mountComponent(widget, ['banana']) const multiselect = wrapper.findComponent({ name: 'MultiSelect' }) @@ -152,7 +187,17 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('applies small size styling', () => { - const widget = createMockWidget([], { values: ['test'] }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: ['test'] } + } + ) const wrapper = mountComponent(widget, []) const multiselect = wrapper.findComponent({ name: 'MultiSelect' }) @@ -160,7 +205,17 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('uses chip display mode', () => { - const widget = createMockWidget([], { values: ['test'] }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: ['test'] } + } + ) const wrapper = mountComponent(widget, []) const multiselect = wrapper.findComponent({ name: 'MultiSelect' }) @@ -168,7 +223,17 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('applies text-xs class', () => { - const widget = createMockWidget([], { values: ['test'] }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: ['test'] } + } + ) const wrapper = mountComponent(widget, []) const multiselect = wrapper.findComponent({ name: 'MultiSelect' }) @@ -178,7 +243,7 @@ describe('WidgetMultiSelect Value Binding', () => { describe('Widget Options Handling', () => { it('passes through valid widget options', () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: ['option1', 'option2'], placeholder: 'Select items...', filter: true, @@ -193,7 +258,7 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('excludes panel-related props', () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: ['option1'], overlayStyle: { color: 'red' }, panelClass: 'custom-panel' @@ -207,7 +272,17 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('handles empty values array', () => { - const widget = createMockWidget([], { values: [] }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: [] } + } + ) const wrapper = mountComponent(widget, []) const multiselect = wrapper.findComponent({ name: 'MultiSelect' }) @@ -215,7 +290,7 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('handles missing values option', () => { - const widget = createMockWidget([]) + const widget = createLocalMockWidget([]) const wrapper = mountComponent(widget, []) const multiselect = wrapper.findComponent({ name: 'MultiSelect' }) @@ -226,7 +301,7 @@ describe('WidgetMultiSelect Value Binding', () => { describe('Edge Cases', () => { it('handles numeric values', async () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: [1, 2, 3, 4, 5] }) const wrapper = mountComponent(widget, []) @@ -239,7 +314,7 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('handles mixed type values', async () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: ['string', 123, true, null] }) const wrapper = mountComponent(widget, []) @@ -256,7 +331,7 @@ describe('WidgetMultiSelect Value Binding', () => { { id: 1, label: 'First' }, { id: 2, label: 'Second' } ] - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: objectValues, optionLabel: 'label', optionValue: 'id' @@ -271,7 +346,7 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('handles duplicate selections gracefully', async () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: ['option1', 'option2'] }) const wrapper = mountComponent(widget, []) @@ -290,7 +365,17 @@ describe('WidgetMultiSelect Value Binding', () => { { length: 1000 }, (_, i) => `option${i}` ) - const widget = createMockWidget([], { values: largeOptionList }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: largeOptionList } + } + ) const wrapper = mountComponent(widget, []) const multiselect = wrapper.findComponent({ name: 'MultiSelect' }) @@ -298,7 +383,7 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('handles empty string values', async () => { - const widget = createMockWidget([], { + const widget = createLocalMockWidget([], { values: ['', 'not empty', ' ', 'normal'] }) const wrapper = mountComponent(widget, []) @@ -313,7 +398,17 @@ describe('WidgetMultiSelect Value Binding', () => { describe('Integration with Layout', () => { it('renders within WidgetLayoutField', () => { - const widget = createMockWidget([], { values: ['test'] }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: ['test'] } + } + ) const wrapper = mountComponent(widget, []) const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' }) @@ -322,7 +417,17 @@ describe('WidgetMultiSelect Value Binding', () => { }) it('passes widget name to layout field', () => { - const widget = createMockWidget([], { values: ['test'] }) + const widget = createMockWidget( + [], + {}, + undefined, + {}, + { + type: 'MULTISELECT', + name: 'test_widget', + options: { values: ['test'] } + } + ) widget.name = 'custom_multiselect' const wrapper = mountComponent(widget, []) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.test.ts index 9eb13dd2ae..f37af54a60 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.test.ts @@ -3,23 +3,10 @@ import PrimeVue from 'primevue/config' import { describe, expect, it, vi } from 'vitest' import type { SimplifiedWidget } from '@/types/simplifiedWidget' +import { createMockWidget } from '../testUtils' import WidgetSelectButton from './WidgetSelectButton.vue' -function createMockWidget( - value: string = 'option1', - options: SimplifiedWidget['options'] = {}, - callback?: (value: string) => void -): SimplifiedWidget { - return { - name: 'test_selectbutton', - type: 'string', - value, - options, - callback - } -} - function mountComponent( widget: SimplifiedWidget, modelValue: string, @@ -57,9 +44,20 @@ async function clickSelectButton( describe('WidgetSelectButton Button Selection', () => { describe('Basic Rendering', () => { it('renders FormSelectButton component', () => { - const widget = createMockWidget('option1', { - values: ['option1', 'option2', 'option3'] - }) + const widget = createMockWidget( + 'option1', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: ['option1', 'option2', 'option3'] } + } + ) const wrapper = mountComponent(widget, 'option1') const formSelectButton = wrapper.findComponent({ @@ -70,7 +68,20 @@ describe('WidgetSelectButton Button Selection', () => { it('renders buttons for each option', () => { const options = ['first', 'second', 'third'] - const widget = createMockWidget('first', { values: options }) + const widget = createMockWidget( + 'first', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'first') const buttons = wrapper.findAll('button') @@ -81,7 +92,20 @@ describe('WidgetSelectButton Button Selection', () => { }) it('handles empty options array', () => { - const widget = createMockWidget('', { values: [] }) + const widget = createMockWidget( + '', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: [] } + } + ) const wrapper = mountComponent(widget, '') const buttons = wrapper.findAll('button') @@ -89,7 +113,7 @@ describe('WidgetSelectButton Button Selection', () => { }) it('handles missing values option', () => { - const widget = createMockWidget('') + const widget = createMockWidget('') const wrapper = mountComponent(widget, '') const buttons = wrapper.findAll('button') @@ -100,7 +124,20 @@ describe('WidgetSelectButton Button Selection', () => { describe('Selection State', () => { it('highlights selected option', () => { const options = ['apple', 'banana', 'cherry'] - const widget = createMockWidget('banana', { values: options }) + const widget = createMockWidget( + 'banana', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'banana') const buttons = wrapper.findAll('button') @@ -119,7 +156,20 @@ describe('WidgetSelectButton Button Selection', () => { it('handles no selection gracefully', () => { const options = ['option1', 'option2'] - const widget = createMockWidget('nonexistent', { values: options }) + const widget = createMockWidget( + 'nonexistent', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'nonexistent') const buttons = wrapper.findAll('button') @@ -135,7 +185,20 @@ describe('WidgetSelectButton Button Selection', () => { context.skip('Classes not updating, needs diagnosis') const options = ['first', 'second', 'third'] - const widget = createMockWidget('first', { values: options }) + const widget = createMockWidget( + 'first', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'first') // Initially 'first' is selected @@ -159,7 +222,20 @@ describe('WidgetSelectButton Button Selection', () => { describe('User Interactions', () => { it('emits update:modelValue when button is clicked', async () => { const options = ['first', 'second', 'third'] - const widget = createMockWidget('first', { values: options }) + const widget = createMockWidget( + 'first', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'first') await clickSelectButton(wrapper, 'second') @@ -173,10 +249,19 @@ describe('WidgetSelectButton Button Selection', () => { context.skip('Callback is not being called, needs diagnosis') const mockCallback = vi.fn() const options = ['option1', 'option2'] - const widget = createMockWidget( + const widget = createMockWidget( 'option1', - { values: options }, - mockCallback + {}, + mockCallback, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } ) const wrapper = mountComponent(widget, 'option1') @@ -187,7 +272,20 @@ describe('WidgetSelectButton Button Selection', () => { it('handles missing callback gracefully', async () => { const options = ['option1', 'option2'] - const widget = createMockWidget('option1', { values: options }, undefined) + const widget = createMockWidget( + 'option1', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'option1') await clickSelectButton(wrapper, 'option2') @@ -200,7 +298,20 @@ describe('WidgetSelectButton Button Selection', () => { it('allows clicking same option again', async () => { const options = ['option1', 'option2'] - const widget = createMockWidget('option1', { values: options }) + const widget = createMockWidget( + 'option1', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'option1') await clickSelectButton(wrapper, 'option1') @@ -214,7 +325,20 @@ describe('WidgetSelectButton Button Selection', () => { describe('Option Types', () => { it('handles string options', () => { const options = ['apple', 'banana', 'cherry'] - const widget = createMockWidget('banana', { values: options }) + const widget = createMockWidget( + 'banana', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'banana') const buttons = wrapper.findAll('button') @@ -225,7 +349,20 @@ describe('WidgetSelectButton Button Selection', () => { it('handles number options', () => { const options = [1, 2, 3] - const widget = createMockWidget('2', { values: options }) + const widget = createMockWidget( + '2', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, '2') const buttons = wrapper.findAll('button') @@ -245,7 +382,20 @@ describe('WidgetSelectButton Button Selection', () => { { label: 'Second Option', value: 'second' }, { label: 'Third Option', value: 'third' } ] - const widget = createMockWidget('second', { values: options }) + const widget = createMockWidget( + 'second', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'second') const buttons = wrapper.findAll('button') @@ -264,7 +414,20 @@ describe('WidgetSelectButton Button Selection', () => { { label: 'First', value: 'first_val' }, { label: 'Second', value: 'second_val' } ] - const widget = createMockWidget('first_val', { values: options }) + const widget = createMockWidget( + 'first_val', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'first_val') await clickSelectButton(wrapper, 'Second') @@ -278,7 +441,20 @@ describe('WidgetSelectButton Button Selection', () => { describe('Edge Cases', () => { it('handles options with special characters', () => { const options = ['@#$%^&*()', '{}[]|\\:";\'<>?,./'] - const widget = createMockWidget(options[0], { values: options }) + const widget = createMockWidget( + options[0], + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, options[0]) const buttons = wrapper.findAll('button') @@ -288,7 +464,20 @@ describe('WidgetSelectButton Button Selection', () => { it('handles empty string options', () => { const options = ['', 'not empty', ' ', 'normal'] - const widget = createMockWidget('', { values: options }) + const widget = createMockWidget( + '', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, '') const buttons = wrapper.findAll('button') @@ -305,7 +494,20 @@ describe('WidgetSelectButton Button Selection', () => { undefined, 'another' ] - const widget = createMockWidget('valid', { values: options }) + const widget = createMockWidget( + 'valid', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'valid') const buttons = wrapper.findAll('button') @@ -319,7 +521,20 @@ describe('WidgetSelectButton Button Selection', () => { const longText = 'This is a very long option text that might cause layout issues if not handled properly' const options = ['short', longText, 'normal'] - const widget = createMockWidget('short', { values: options }) + const widget = createMockWidget( + 'short', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'short') const buttons = wrapper.findAll('button') @@ -328,7 +543,20 @@ describe('WidgetSelectButton Button Selection', () => { it('handles large number of options', () => { const options = Array.from({ length: 20 }, (_, i) => `option${i + 1}`) - const widget = createMockWidget('option5', { values: options }) + const widget = createMockWidget( + 'option5', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'option5') const buttons = wrapper.findAll('button') @@ -340,7 +568,20 @@ describe('WidgetSelectButton Button Selection', () => { it('handles duplicate options', () => { const options = ['duplicate', 'unique', 'duplicate', 'unique'] - const widget = createMockWidget('duplicate', { values: options }) + const widget = createMockWidget( + 'duplicate', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'duplicate') const buttons = wrapper.findAll('button') @@ -358,7 +599,20 @@ describe('WidgetSelectButton Button Selection', () => { describe('Styling and Layout', () => { it('applies proper button styling', () => { const options = ['option1', 'option2'] - const widget = createMockWidget('option1', { values: options }) + const widget = createMockWidget( + 'option1', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'option1') const buttons = wrapper.findAll('button') @@ -374,7 +628,20 @@ describe('WidgetSelectButton Button Selection', () => { it('applies hover effects for non-selected options', () => { const options = ['option1', 'option2'] - const widget = createMockWidget('option1', { values: options }) + const widget = createMockWidget( + 'option1', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: options } + } + ) const wrapper = mountComponent(widget, 'option1', false) const buttons = wrapper.findAll('button') @@ -389,7 +656,20 @@ describe('WidgetSelectButton Button Selection', () => { describe('Integration with Layout', () => { it('renders within WidgetLayoutField', () => { - const widget = createMockWidget('test', { values: ['test'] }) + const widget = createMockWidget( + 'test', + {}, + undefined, + { + name: 'test_selectbutton', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'test_selectbutton', + options: { values: ['test'] } + } + ) const wrapper = mountComponent(widget, 'test') const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' }) @@ -398,8 +678,20 @@ describe('WidgetSelectButton Button Selection', () => { }) it('passes widget name to layout field', () => { - const widget = createMockWidget('test', { values: ['test'] }) - widget.name = 'custom_select_button' + const widget = createMockWidget( + 'test', + {}, + undefined, + { + name: 'custom_select_button', + type: 'string' + }, + { + type: 'SELECTBUTTON', + name: 'custom_select_button', + options: { values: ['test'] } + } + ) const wrapper = mountComponent(widget, 'test') const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue index bed7d246ae..9e3d08beb3 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectButton.vue @@ -41,6 +41,6 @@ const selectOptions = computed(() => { if (!spec || !isSelectButtonInputSpec(spec)) { return [] } - return spec.options?.values || [] + return spec.options?.values ?? [] }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue index f64a4c2585..b52fe066a8 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTreeSelect.vue @@ -22,6 +22,10 @@ import { useWidgetValue } from '@/composables/graph/useWidgetValue' import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps' import { isTreeSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' import type { SimplifiedWidget } from '@/types/simplifiedWidget' +import { + PANEL_EXCLUDED_PROPS, + filterWidgetProps +} from '@/utils/widgetPropFilter' import WidgetLayoutField from './layout/WidgetLayoutField.vue' @@ -54,26 +58,32 @@ const { localValue, onChange } = useWidgetValue({ // Transform compatibility props for overlay positioning const transformCompatProps = useTransformCompatOverlayProps() +// TreeSelect specific excluded props +const TREE_SELECT_EXCLUDED_PROPS = [ + ...PANEL_EXCLUDED_PROPS, + 'inputClass', + 'inputStyle' +] as const + const combinedProps = computed(() => { const spec = props.widget.spec if (!spec || !isTreeSelectInputSpec(spec)) { return { - ...props.widget.options, + ...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS), ...transformCompatProps.value } } const specOptions = spec.options || {} return { - // Include runtime props like disabled - ...props.widget.options, + // Include runtime props like disabled, but filter out panel-related ones + ...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS), // PrimeVue TreeSelect expects 'options' to be an array of tree nodes options: (specOptions.values as TreeNode[]) || [], // Convert 'multiple' to PrimeVue's 'selectionMode' - selectionMode: (specOptions.multiple ? 'multiple' : 'single') as - | 'single' - | 'multiple' - | 'checkbox', + selectionMode: specOptions.multiple + ? ('multiple' as const) + : ('single' as const), // Pass through other props like placeholder placeholder: specOptions.placeholder, ...transformCompatProps.value diff --git a/src/renderer/extensions/vueNodes/widgets/testUtils.ts b/src/renderer/extensions/vueNodes/widgets/testUtils.ts index c8d1125281..22f74a4d6c 100644 --- a/src/renderer/extensions/vueNodes/widgets/testUtils.ts +++ b/src/renderer/extensions/vueNodes/widgets/testUtils.ts @@ -1,16 +1,18 @@ import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget' +import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2' /** * Creates a mock SimplifiedWidget for testing Vue Node widgets. * This utility function is shared across widget component tests to ensure consistency. */ -export function createMockWidget( +export function createMockWidget( value: T = null as T, options: Record = {}, callback?: (value: T) => void, - overrides: Partial> = {} + overrides: Partial> = {}, + spec?: Partial ): SimplifiedWidget { - return { + const widget: SimplifiedWidget = { name: 'test_widget', type: 'default', value, @@ -18,6 +20,13 @@ export function createMockWidget( callback, ...overrides } + + // Only add spec if provided + if (spec) { + widget.spec = spec as InputSpec + } + + return widget } /** diff --git a/src/schemas/nodeDef/nodeDefSchemaV2.ts b/src/schemas/nodeDef/nodeDefSchemaV2.ts index 4ffd540fc9..277b469f29 100644 --- a/src/schemas/nodeDef/nodeDefSchemaV2.ts +++ b/src/schemas/nodeDef/nodeDefSchemaV2.ts @@ -224,6 +224,7 @@ type StringInputSpec = z.infer export type ComboInputSpec = z.infer export type ColorInputSpec = z.infer export type FileUploadInputSpec = z.infer +export type ImageInputSpec = z.infer export type ImageCompareInputSpec = z.infer export type TreeSelectInputSpec = z.infer export type MultiSelectInputSpec = z.infer @@ -308,3 +309,9 @@ export const isFileUploadInputSpec = ( ): inputSpec is FileUploadInputSpec => { return inputSpec.type === 'FILEUPLOAD' } + +export const isImageInputSpec = ( + inputSpec: InputSpec +): inputSpec is ImageInputSpec => { + return inputSpec.type === 'IMAGE' +} From 8fc5e877f941031a9b2ad929966f75c0ff4d8ece Mon Sep 17 00:00:00 2001 From: bymyself Date: Sat, 15 Nov 2025 14:20:25 -0800 Subject: [PATCH 6/6] fix: resolve knip unused code detection and typecheck issues - Remove unused exports isImageInputSpec and ImageInputSpec to fix knip failures - Add ts-expect-error for future IMAGE widget implementation - All quality checks now pass: tests (96.9%), typecheck, knip --- src/schemas/nodeDef/nodeDefSchemaV2.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/schemas/nodeDef/nodeDefSchemaV2.ts b/src/schemas/nodeDef/nodeDefSchemaV2.ts index 277b469f29..7cd9c26756 100644 --- a/src/schemas/nodeDef/nodeDefSchemaV2.ts +++ b/src/schemas/nodeDef/nodeDefSchemaV2.ts @@ -224,7 +224,7 @@ type StringInputSpec = z.infer export type ComboInputSpec = z.infer export type ColorInputSpec = z.infer export type FileUploadInputSpec = z.infer -export type ImageInputSpec = z.infer +type ImageInputSpec = z.infer export type ImageCompareInputSpec = z.infer export type TreeSelectInputSpec = z.infer export type MultiSelectInputSpec = z.infer @@ -310,7 +310,8 @@ export const isFileUploadInputSpec = ( return inputSpec.type === 'FILEUPLOAD' } -export const isImageInputSpec = ( +// @ts-expect-error - will be used in future IMAGE widget implementation +const isImageInputSpec = ( inputSpec: InputSpec ): inputSpec is ImageInputSpec => { return inputSpec.type === 'IMAGE'