mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-25 23:25:02 +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>
190 lines
6.3 KiB
TypeScript
190 lines
6.3 KiB
TypeScript
import { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
|
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
|
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
|
import { describe, expect, it, beforeEach } from 'vitest'
|
|
import { flatAndCategorizeSelectedItems, searchWidgets } from './shared'
|
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
|
|
|
describe('searchWidgets', () => {
|
|
const createWidget = (
|
|
name: string,
|
|
type: string,
|
|
value?: string,
|
|
label?: string
|
|
): { widget: IBaseWidget } => ({
|
|
widget: {
|
|
name,
|
|
type,
|
|
value,
|
|
label
|
|
} as IBaseWidget
|
|
})
|
|
|
|
it('should return all widgets when query is empty', () => {
|
|
const widgets = [
|
|
createWidget('width', 'number', '100'),
|
|
createWidget('height', 'number', '200')
|
|
]
|
|
const result = searchWidgets(widgets, '')
|
|
expect(result).toEqual(widgets)
|
|
})
|
|
|
|
it('should filter widgets by name, label, type, or value', () => {
|
|
const widgets = [
|
|
createWidget('width', 'number', '100', 'Size Control'),
|
|
createWidget('height', 'slider', '200', 'Image Height'),
|
|
createWidget('quality', 'text', 'high', 'Quality')
|
|
]
|
|
|
|
expect(searchWidgets(widgets, 'width')).toHaveLength(1)
|
|
expect(searchWidgets(widgets, 'slider')).toHaveLength(1)
|
|
expect(searchWidgets(widgets, 'image')).toHaveLength(1)
|
|
})
|
|
|
|
it('should support fuzzy matching (e.g., "high" matches both "height" and value "high")', () => {
|
|
const widgets = [
|
|
createWidget('width', 'number', '100', 'Size Control'),
|
|
createWidget('height', 'slider', '200', 'Image Height'),
|
|
createWidget('quality', 'text', 'high', 'Quality')
|
|
]
|
|
|
|
const results = searchWidgets(widgets, 'high')
|
|
expect(results).toHaveLength(2)
|
|
expect(results.some((r) => r.widget.name === 'height')).toBe(true)
|
|
expect(results.some((r) => r.widget.name === 'quality')).toBe(true)
|
|
})
|
|
|
|
it('should handle multiple search words', () => {
|
|
const widgets = [
|
|
createWidget('width', 'number', '100', 'Image Width'),
|
|
createWidget('height', 'number', '200', 'Image Height')
|
|
]
|
|
const result = searchWidgets(widgets, 'image width')
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].widget.name).toBe('width')
|
|
})
|
|
|
|
it('should be case insensitive', () => {
|
|
const widgets = [createWidget('Width', 'Number', '100', 'Image Width')]
|
|
const result = searchWidgets(widgets, 'IMAGE width')
|
|
expect(result).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('flatAndCategorizeSelectedItems', () => {
|
|
let testGroup1: LGraphGroup
|
|
let testGroup2: LGraphGroup
|
|
let testNode1: LGraphNode
|
|
let testNode2: LGraphNode
|
|
let testNode3: LGraphNode
|
|
|
|
beforeEach(() => {
|
|
testGroup1 = new LGraphGroup('Group 1', 1)
|
|
testGroup2 = new LGraphGroup('Group 2', 2)
|
|
testNode1 = new LGraphNode('Node 1')
|
|
testNode2 = new LGraphNode('Node 2')
|
|
testNode3 = new LGraphNode('Node 3')
|
|
})
|
|
|
|
it('should return empty arrays for empty input', () => {
|
|
const result = flatAndCategorizeSelectedItems([])
|
|
|
|
expect(result.all).toEqual([])
|
|
expect(result.nodes).toEqual([])
|
|
expect(result.groups).toEqual([])
|
|
expect(result.others).toEqual([])
|
|
expect(result.nodeToParentGroup.size).toBe(0)
|
|
})
|
|
|
|
it('should categorize nodes', () => {
|
|
const result = flatAndCategorizeSelectedItems([testNode1])
|
|
|
|
expect(result.all).toEqual([testNode1])
|
|
expect(result.nodes).toEqual([testNode1])
|
|
expect(result.groups).toEqual([])
|
|
expect(result.others).toEqual([])
|
|
expect(result.nodeToParentGroup.size).toBe(0)
|
|
})
|
|
|
|
it('should categorize single group without children', () => {
|
|
const result = flatAndCategorizeSelectedItems([testGroup1])
|
|
|
|
expect(result.all).toEqual([testGroup1])
|
|
expect(result.nodes).toEqual([])
|
|
expect(result.groups).toEqual([testGroup1])
|
|
expect(result.others).toEqual([])
|
|
})
|
|
|
|
it('should flatten group with child nodes', () => {
|
|
testGroup1._children.add(testNode1)
|
|
testGroup1._children.add(testNode2)
|
|
|
|
const result = flatAndCategorizeSelectedItems([testGroup1])
|
|
|
|
expect(result.all).toEqual([testGroup1, testNode1, testNode2])
|
|
expect(result.nodes).toEqual([testNode1, testNode2])
|
|
expect(result.groups).toEqual([testGroup1])
|
|
expect(result.nodeToParentGroup.get(testNode1)).toBe(testGroup1)
|
|
expect(result.nodeToParentGroup.get(testNode2)).toBe(testGroup1)
|
|
})
|
|
|
|
it('should handle nested groups', () => {
|
|
testGroup1._children.add(testGroup2)
|
|
testGroup2._children.add(testNode1)
|
|
|
|
const result = flatAndCategorizeSelectedItems([testGroup1])
|
|
|
|
expect(result.all).toEqual([testGroup1, testGroup2, testNode1])
|
|
expect(result.nodes).toEqual([testNode1])
|
|
expect(result.groups).toEqual([testGroup1, testGroup2])
|
|
expect(result.nodeToParentGroup.get(testNode1)).toBe(testGroup2)
|
|
})
|
|
|
|
it('should handle mixed selection of nodes and groups', () => {
|
|
testGroup1._children.add(testNode2)
|
|
|
|
const result = flatAndCategorizeSelectedItems([
|
|
testNode1,
|
|
testGroup1,
|
|
testNode3
|
|
])
|
|
|
|
expect(result.all).toContain(testNode1)
|
|
expect(result.all).toContain(testNode2)
|
|
expect(result.all).toContain(testNode3)
|
|
expect(result.all).toContain(testGroup1)
|
|
|
|
expect(result.nodes).toEqual([testNode1, testNode2, testNode3])
|
|
expect(result.groups).toEqual([testGroup1])
|
|
expect(result.nodeToParentGroup.get(testNode1)).toBeUndefined()
|
|
expect(result.nodeToParentGroup.get(testNode2)).toBe(testGroup1)
|
|
expect(result.nodeToParentGroup.get(testNode3)).toBeUndefined()
|
|
})
|
|
|
|
it('should remove duplicate items across group and direct selection', () => {
|
|
testGroup1._children.add(testNode1)
|
|
|
|
const result = flatAndCategorizeSelectedItems([testGroup1, testNode1])
|
|
|
|
expect(result.all).toEqual([testGroup1, testNode1])
|
|
expect(result.nodes).toEqual([testNode1])
|
|
expect(result.groups).toEqual([testGroup1])
|
|
expect(result.nodeToParentGroup.get(testNode1)).toBe(testGroup1)
|
|
})
|
|
|
|
it('should handle non-node/non-group items as others', () => {
|
|
const unknownItem = { pos: [0, 0], size: [100, 100] } as Positionable
|
|
|
|
const result = flatAndCategorizeSelectedItems([
|
|
testNode1,
|
|
unknownItem,
|
|
testGroup1
|
|
])
|
|
|
|
expect(result.nodes).toEqual([testNode1])
|
|
expect(result.groups).toEqual([testGroup1])
|
|
expect(result.others).toEqual([unknownItem])
|
|
expect(result.all).not.toContain(unknownItem)
|
|
})
|
|
})
|