mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 00:20:15 +00:00
## Summary
This PR removes `any` types from widgets, services, stores, and test
files, replacing them with proper TypeScript types.
### Key Changes
#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across widgets and services
- Added proper type imports (TgpuRoot, Point, StyleValue, etc.)
- Created typed interfaces (NumericWidgetOptions, TestWindow,
ImportFailureDetail, etc.)
- Fixed function return types to be non-nullable where appropriate
- Added type guards and null checks instead of non-null assertions
- Used `ComponentProps` from vue-component-type-helpers for component
testing
#### Widget System
- Added index signature to IWidgetOptions for Record compatibility
- Centralized disabled logic in WidgetInputNumberInput
- Moved template type assertions to computed properties
- Fixed ComboWidget getOptionLabel type assertions
- Improved remote widget type handling with runtime checks
#### Services & Stores
- Fixed getOrCreateViewer to return non-nullable values
- Updated addNodeOnGraph to use specific options type `{ pos?: Point }`
- Added proper type assertions for settings store retrieval
- Fixed executionIdToCurrentId return type (string | undefined)
#### Test Infrastructure
- Exported GraphOrSubgraph from litegraph barrel to avoid circular
dependencies
- Updated test fixtures with proper TypeScript types (TestInfo,
LGraphNode)
- Replaced loose Record types with ComponentProps in tests
- Added proper error handling in WebSocket fixture
#### Code Organization
- Created shared i18n-types module for locale data types
- Made ImportFailureDetail non-exported (internal use only)
- Added @public JSDoc tag to ElectronWindow type
- Fixed console.log usage in scripts to use allowed methods
### Files Changed
**Widgets & Components:**
-
src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
- src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.ts
- src/lib/litegraph/src/widgets/ComboWidget.ts
- src/lib/litegraph/src/types/widgets.ts
- src/components/common/LazyImage.vue
- src/components/load3d/Load3dViewerContent.vue
**Services & Stores:**
- src/services/litegraphService.ts
- src/services/load3dService.ts
- src/services/colorPaletteService.ts
- src/stores/maskEditorStore.ts
- src/stores/nodeDefStore.ts
- src/platform/settings/settingStore.ts
- src/platform/workflow/management/stores/workflowStore.ts
**Composables & Utils:**
- src/composables/node/useWatchWidget.ts
- src/composables/useCanvasDrop.ts
- src/utils/widgetPropFilter.ts
- src/utils/queueDisplay.ts
- src/utils/envUtil.ts
**Test Files:**
- browser_tests/fixtures/ComfyPage.ts
- browser_tests/fixtures/ws.ts
- browser_tests/tests/actionbar.spec.ts
-
src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts
- src/lib/litegraph/src/subgraph/subgraphUtils.test.ts
- src/components/rightSidePanel/shared.test.ts
- src/platform/cloud/subscription/composables/useSubscription.test.ts
-
src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts
**Scripts & Types:**
- scripts/i18n-types.ts (new shared module)
- scripts/diff-i18n.ts
- scripts/check-unused-i18n-keys.ts
- src/workbench/extensions/manager/types/conflictDetectionTypes.ts
- src/types/algoliaTypes.ts
- src/types/simplifiedWidget.ts
**Infrastructure:**
- src/lib/litegraph/src/litegraph.ts (added GraphOrSubgraph export)
- src/lib/litegraph/src/infrastructure/CustomEventTarget.ts
- src/platform/assets/services/assetService.ts
**Stories:**
- apps/desktop-ui/src/views/InstallView.stories.ts
- src/components/queue/job/JobDetailsPopover.stories.ts
**Extension Manager:**
- src/workbench/extensions/manager/composables/useConflictDetection.ts
- src/workbench/extensions/manager/composables/useManagerQueue.ts
- src/workbench/extensions/manager/services/comfyManagerService.ts
- src/workbench/extensions/manager/utils/conflictMessageUtil.ts
### Testing
- [x] All TypeScript type checking passes (`pnpm typecheck`)
- [x] ESLint passes without errors (`pnpm lint`)
- [x] Format checks pass (`pnpm format:check`)
- [x] Knip (unused exports) passes (`pnpm knip`)
- [x] Pre-commit and pre-push hooks pass
Part of the "Road to No Explicit Any" initiative.
### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496
- Part 9: #8498
- Part 10: #8499
---------
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
182 lines
5.7 KiB
TypeScript
182 lines
5.7 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import PrimeVue from 'primevue/config'
|
|
import InputText from 'primevue/inputtext'
|
|
import type { InputTextProps } from 'primevue/inputtext'
|
|
import Textarea from 'primevue/textarea'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
|
|
|
import WidgetInputText from './WidgetInputText.vue'
|
|
|
|
describe('WidgetInputText Value Binding', () => {
|
|
const createMockWidget = (
|
|
value: string = 'default',
|
|
options: Partial<InputTextProps> = {},
|
|
callback?: (value: string) => void
|
|
): SimplifiedWidget<string> => ({
|
|
name: 'test_input',
|
|
type: 'string',
|
|
value,
|
|
options: options as IWidgetOptions,
|
|
callback
|
|
})
|
|
|
|
const mountComponent = (
|
|
widget: SimplifiedWidget<string>,
|
|
modelValue: string,
|
|
readonly = false
|
|
) => {
|
|
return mount(WidgetInputText, {
|
|
global: {
|
|
plugins: [PrimeVue],
|
|
components: { InputText, Textarea }
|
|
},
|
|
props: {
|
|
widget,
|
|
modelValue,
|
|
readonly
|
|
}
|
|
})
|
|
}
|
|
|
|
const setInputValueAndTrigger = async (
|
|
wrapper: ReturnType<typeof mount>,
|
|
value: string,
|
|
trigger: 'blur' | 'keydown.enter' = 'blur'
|
|
) => {
|
|
const input = wrapper.find('input[type="text"]')
|
|
if (!(input.element instanceof HTMLInputElement)) {
|
|
throw new Error('Input element not found or is not an HTMLInputElement')
|
|
}
|
|
await input.setValue(value)
|
|
await input.trigger(trigger)
|
|
return input
|
|
}
|
|
|
|
describe('Vue Event Emission', () => {
|
|
it('emits Vue event when input value changes on blur', async () => {
|
|
const widget = createMockWidget('hello')
|
|
const wrapper = mountComponent(widget, 'hello')
|
|
|
|
await setInputValueAndTrigger(wrapper, 'world', 'blur')
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain('world')
|
|
})
|
|
|
|
it('emits Vue event when enter key is pressed', async () => {
|
|
const widget = createMockWidget('initial')
|
|
const wrapper = mountComponent(widget, 'initial')
|
|
|
|
await setInputValueAndTrigger(wrapper, 'new value', 'keydown.enter')
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain('new value')
|
|
})
|
|
|
|
it('handles empty string values', async () => {
|
|
const widget = createMockWidget('something')
|
|
const wrapper = mountComponent(widget, 'something')
|
|
|
|
await setInputValueAndTrigger(wrapper, '')
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain('')
|
|
})
|
|
|
|
it('handles special characters correctly', async () => {
|
|
const widget = createMockWidget('normal')
|
|
const wrapper = mountComponent(widget, 'normal')
|
|
|
|
const specialText = 'special @#$%^&*()[]{}|\\:";\'<>?,./'
|
|
await setInputValueAndTrigger(wrapper, specialText)
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain(specialText)
|
|
})
|
|
|
|
it('handles missing callback gracefully', async () => {
|
|
const widget = createMockWidget('test', {}, undefined)
|
|
const wrapper = mountComponent(widget, 'test')
|
|
|
|
await setInputValueAndTrigger(wrapper, 'new value')
|
|
|
|
// Should still emit Vue event
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain('new value')
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('emits update:modelValue on blur', async () => {
|
|
const widget = createMockWidget('original')
|
|
const wrapper = mountComponent(widget, 'original')
|
|
|
|
await setInputValueAndTrigger(wrapper, 'updated')
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain('updated')
|
|
})
|
|
|
|
it('emits update:modelValue on enter key', async () => {
|
|
const widget = createMockWidget('start')
|
|
const wrapper = mountComponent(widget, 'start')
|
|
|
|
await setInputValueAndTrigger(wrapper, 'finish', 'keydown.enter')
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain('finish')
|
|
})
|
|
})
|
|
|
|
describe('Component Rendering', () => {
|
|
it('always renders InputText component', () => {
|
|
const widget = createMockWidget('test value')
|
|
const wrapper = mountComponent(widget, 'test value')
|
|
|
|
// WidgetInputText always uses InputText, not Textarea
|
|
const input = wrapper.find('input[type="text"]')
|
|
expect(input.exists()).toBe(true)
|
|
|
|
// Should not render textarea (that's handled by WidgetTextarea component)
|
|
const textarea = wrapper.find('textarea')
|
|
expect(textarea.exists()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('handles very long strings', async () => {
|
|
const widget = createMockWidget('short')
|
|
const wrapper = mountComponent(widget, 'short')
|
|
|
|
const longString = 'a'.repeat(10000)
|
|
await setInputValueAndTrigger(wrapper, longString)
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain(longString)
|
|
})
|
|
|
|
it('handles unicode characters', async () => {
|
|
const widget = createMockWidget('ascii')
|
|
const wrapper = mountComponent(widget, 'ascii')
|
|
|
|
const unicodeText = '🎨 Unicode: αβγ 中文 العربية 🚀'
|
|
await setInputValueAndTrigger(wrapper, unicodeText)
|
|
|
|
const emitted = wrapper.emitted('update:modelValue')
|
|
expect(emitted).toBeDefined()
|
|
expect(emitted![0]).toContain(unicodeText)
|
|
})
|
|
})
|
|
})
|