mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-18 01:38:03 +00:00
Compare commits
25 Commits
split/bill
...
glary/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8a8ef1a51 | ||
|
|
0610afa851 | ||
|
|
4999e3586b | ||
|
|
f2c8be3233 | ||
|
|
6a98abe3b5 | ||
|
|
90e2cda856 | ||
|
|
784737f691 | ||
|
|
b9fa13b895 | ||
|
|
08611550d1 | ||
|
|
6a02ee339b | ||
|
|
cbbcadad11 | ||
|
|
1a428e0a0e | ||
|
|
460f64f950 | ||
|
|
36c5fcf028 | ||
|
|
6af84bddf9 | ||
|
|
1562ddbe0a | ||
|
|
ae41efb202 | ||
|
|
755daab02b | ||
|
|
11386a768d | ||
|
|
b6111cd25f | ||
|
|
604656fb28 | ||
|
|
86b371c273 | ||
|
|
b8d9d3ebd6 | ||
|
|
4b16fc03c2 | ||
|
|
bf66b8f157 |
@@ -121,6 +121,16 @@ Reserve `toPass()` for blocks with multiple assertions or complex async logic th
|
||||
| Context menu empty or wrong items | Node not selected | Select node first: `vueNodes.selectNode()` or `nodeRef.click('title')` |
|
||||
| `navigateIntoSubgraph` timeout | Node too small in test asset JSON | Use node size `[400, 200]` minimum |
|
||||
|
||||
## Backend Test Nodes (DevTools)
|
||||
|
||||
When a test needs a node, input shape, or backend behavior that no production node exposes, prefer **adding a node to `tools/devtools/nodes/`** over intercepting `/object_info` from the test.
|
||||
|
||||
- Add the new class to the appropriate file (e.g. `inputs.py` for input-shape variants), register it in that file's `NODE_CLASS_MAPPINGS` / `NODE_DISPLAY_NAME_MAPPINGS` with a `DevTools` prefix, and re-export it through `nodes/__init__.py` and `dev_nodes.py`.
|
||||
- In the spec, prefer real user actions for adding the node: `await comfyPage.searchBoxV2.addNode('Node Display Name')` (see `vueNodes/widgets/color/colorWidget.spec.ts`). Reach for `comfyPage.nodeOps.addNode(...)` only when the test is not specifically about the add flow and a programmatic shortcut keeps the test focused (e.g. `vueNodes/widgets/legacy.spec.ts`).
|
||||
- Reserve `page.route(/\/object_info$/, ...)` for cases where the test needs to mutate an existing production node's shape mid-flight (e.g. `propertiesPanel/errorsTabMissingModels.spec.ts`).
|
||||
|
||||
Devtools nodes are loaded automatically into the test ComfyUI instance, so no fixture wiring is required.
|
||||
|
||||
## After Making Changes
|
||||
|
||||
- Run `pnpm typecheck:browser` after modifying TypeScript files in this directory
|
||||
|
||||
28
browser_tests/assets/vueNodes/color-widget-default.json
Normal file
28
browser_tests/assets/vueNodes/color-widget-default.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id": "c0107e57-c01047-c01047-c01047-c0101010c010",
|
||||
"revision": 0,
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "DevToolsNodeWithColorInput",
|
||||
"pos": [400, 300],
|
||||
"size": [270, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithColorInput"
|
||||
},
|
||||
"widgets_values": ["#ff00ff"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
const COLOR_NODE_DISPLAY_NAME = 'Node With Color Input'
|
||||
const DECLARED_DEFAULT = '#00ff00'
|
||||
const PERSISTED_VALUE = '#ff00ff'
|
||||
|
||||
test.describe('Vue Color Widget defaults', { tag: '@vue-nodes' }, () => {
|
||||
test('respects the declared default value in the input spec', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.searchBoxV2.addNode(COLOR_NODE_DISPLAY_NAME)
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeByTitle(COLOR_NODE_DISPLAY_NAME)
|
||||
const colorTrigger = node.getByRole('button', {
|
||||
name: new RegExp(DECLARED_DEFAULT, 'i')
|
||||
})
|
||||
|
||||
await expect(colorTrigger).toBeVisible()
|
||||
})
|
||||
|
||||
test('restores a saved color value when loading a workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('vueNodes/color-widget-default')
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeByTitle(COLOR_NODE_DISPLAY_NAME)
|
||||
const colorTrigger = node.getByRole('button', {
|
||||
name: new RegExp(PERSISTED_VALUE, 'i')
|
||||
})
|
||||
|
||||
await expect(colorTrigger).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -4,16 +4,12 @@ import { describe, expect, it } from 'vitest'
|
||||
import { defineComponent, nextTick, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { ChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import type { ChartWidgetOptions } from './WidgetChart.types'
|
||||
import WidgetChart from './WidgetChart.vue'
|
||||
import { createMockWidget } from './widgetTestUtils'
|
||||
|
||||
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']> &
|
||||
IWidgetOptions
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { ChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
export type ChartWidgetOptions = IWidgetOptions & {
|
||||
type?: ChartInputSpec['chartType']
|
||||
}
|
||||
@@ -16,12 +16,9 @@ import type { ChartData } from 'chart.js'
|
||||
import Chart from 'primevue/chart'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { ChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']> &
|
||||
IWidgetOptions
|
||||
import type { ChartWidgetOptions } from './WidgetChart.types'
|
||||
|
||||
const value = defineModel<ChartData>({ required: true })
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useChartWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useChartWidget'
|
||||
import type {
|
||||
ChartInputSpec,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
function createMockNode(): {
|
||||
node: LGraphNode
|
||||
addWidget: ReturnType<typeof vi.spyOn>
|
||||
} {
|
||||
const node = new LGraphNode('TestChartNode')
|
||||
const addWidget = vi
|
||||
.spyOn(node, 'addWidget')
|
||||
.mockImplementation((type, name, value, _callback, options) => {
|
||||
const widget = {
|
||||
type,
|
||||
name,
|
||||
value,
|
||||
options: typeof options === 'string' ? { property: options } : options,
|
||||
y: 0
|
||||
} as IBaseWidget
|
||||
node.widgets ??= []
|
||||
node.widgets.push(widget)
|
||||
return widget
|
||||
})
|
||||
return { node, addWidget }
|
||||
}
|
||||
|
||||
function createChartSpec(
|
||||
overrides: Partial<ChartInputSpec> = {}
|
||||
): ChartInputSpec {
|
||||
return { type: 'CHART', name: 'chart', ...overrides }
|
||||
}
|
||||
|
||||
describe('useChartWidget', () => {
|
||||
it('uses the declared chartType and data from the input spec', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
const data = { series: [1, 2, 3] }
|
||||
|
||||
const widget = useChartWidget()(
|
||||
node,
|
||||
createChartSpec({ chartType: 'bar', data })
|
||||
)
|
||||
|
||||
expect(addWidget).toHaveBeenCalledWith(
|
||||
'chart',
|
||||
'chart',
|
||||
data,
|
||||
expect.any(Function),
|
||||
{ serialize: true, type: 'bar' }
|
||||
)
|
||||
expect(widget.value).toBe(data)
|
||||
})
|
||||
|
||||
it('defaults to a line chart with empty data', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
|
||||
const widget = useChartWidget()(node, createChartSpec())
|
||||
|
||||
expect(addWidget).toHaveBeenCalledWith(
|
||||
'chart',
|
||||
'chart',
|
||||
{},
|
||||
expect.any(Function),
|
||||
{ serialize: true, type: 'line' }
|
||||
)
|
||||
expect(widget.type).toBe('chart')
|
||||
})
|
||||
|
||||
it('throws when the input spec is not a chart spec', () => {
|
||||
const { node } = createMockNode()
|
||||
const inputSpec = { type: 'STRING', name: 'chart' } as unknown as InputSpec
|
||||
|
||||
expect(() => useChartWidget()(node, inputSpec)).toThrow(
|
||||
'Invalid input spec for chart widget'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,7 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IChartWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { isChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type {
|
||||
ChartInputSpec,
|
||||
InputSpec as InputSpecV2
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
export const useChartWidget = (): ComfyWidgetConstructorV2 => {
|
||||
@@ -13,15 +10,17 @@ export const useChartWidget = (): ComfyWidgetConstructorV2 => {
|
||||
throw new Error('Invalid input spec for chart widget')
|
||||
}
|
||||
|
||||
const { name, options = {} } = inputSpec as ChartInputSpec
|
||||
const { name, chartType = 'line', data = {} } = inputSpec
|
||||
|
||||
const chartType = options.type || 'line'
|
||||
const widgetOptions = { serialize: true, type: chartType }
|
||||
|
||||
const widget = node.addWidget('chart', name, options.data || {}, () => {}, {
|
||||
serialize: true,
|
||||
type: chartType,
|
||||
...options
|
||||
}) as IChartWidget
|
||||
const widget = node.addWidget(
|
||||
'chart',
|
||||
name,
|
||||
data,
|
||||
() => {},
|
||||
widgetOptions
|
||||
) as IChartWidget
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
@@ -1,76 +1,101 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
IColorWidget,
|
||||
IWidgetOptions
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useColorWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useColorWidget'
|
||||
import type {
|
||||
ColorInputSpec,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
function createMockNode(): LGraphNode {
|
||||
const widgets: IColorWidget[] = []
|
||||
const addWidget = vi.fn(
|
||||
(
|
||||
type: string,
|
||||
name: string,
|
||||
value: string,
|
||||
_callback: () => void,
|
||||
options: IWidgetOptions
|
||||
) => {
|
||||
const DECLARED_DEFAULT = '#00ff00'
|
||||
const BLACK_FALLBACK = '#000000'
|
||||
|
||||
function createMockNode(): {
|
||||
node: LGraphNode
|
||||
addWidget: ReturnType<typeof vi.spyOn>
|
||||
} {
|
||||
const node = new LGraphNode('TestColorNode')
|
||||
const addWidget = vi
|
||||
.spyOn(node, 'addWidget')
|
||||
.mockImplementation((type, name, value, _callback, options) => {
|
||||
const widget = {
|
||||
type,
|
||||
name,
|
||||
value,
|
||||
options,
|
||||
callback: _callback
|
||||
} as unknown as IColorWidget
|
||||
widgets.push(widget)
|
||||
options: typeof options === 'string' ? { property: options } : options,
|
||||
y: 0
|
||||
} as IBaseWidget
|
||||
node.widgets ??= []
|
||||
node.widgets.push(widget)
|
||||
return widget
|
||||
}
|
||||
)
|
||||
|
||||
return { widgets, addWidget } as unknown as LGraphNode
|
||||
})
|
||||
return { node, addWidget }
|
||||
}
|
||||
|
||||
const colorSpec: InputSpec = {
|
||||
type: 'COLOR',
|
||||
name: 'color',
|
||||
default: '#ffffff',
|
||||
socketless: true
|
||||
function createColorSpec(
|
||||
overrides: Partial<ColorInputSpec> = {}
|
||||
): ColorInputSpec {
|
||||
return { type: 'COLOR', name: 'color', ...overrides }
|
||||
}
|
||||
|
||||
describe('useColorWidget', () => {
|
||||
it('reads the top-level default from the V2 spec', () => {
|
||||
const node = createMockNode()
|
||||
const widget = useColorWidget()(node, colorSpec)
|
||||
expect(widget.value).toBe('#ffffff')
|
||||
it('uses the declared default from the input spec', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
const inputSpec = createColorSpec({ default: DECLARED_DEFAULT })
|
||||
|
||||
const widget = useColorWidget()(node, inputSpec)
|
||||
|
||||
expect(addWidget).toHaveBeenCalledWith(
|
||||
'color',
|
||||
'color',
|
||||
DECLARED_DEFAULT,
|
||||
expect.any(Function),
|
||||
{ serialize: true }
|
||||
)
|
||||
expect(widget.value).toBe(DECLARED_DEFAULT)
|
||||
})
|
||||
|
||||
it('falls back to nested options.default when top-level default is absent', () => {
|
||||
const node = createMockNode()
|
||||
const widget = useColorWidget()(node, {
|
||||
type: 'COLOR',
|
||||
name: 'color',
|
||||
options: { default: '#abcdef' }
|
||||
} as InputSpec)
|
||||
expect(widget.value).toBe('#abcdef')
|
||||
it('falls back to black when no default is supplied', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
|
||||
const widget = useColorWidget()(node, createColorSpec())
|
||||
|
||||
expect(addWidget).toHaveBeenCalledOnce()
|
||||
expect(widget.type).toBe('color')
|
||||
expect(widget.name).toBe('color')
|
||||
expect(widget.value).toBe(BLACK_FALLBACK)
|
||||
})
|
||||
|
||||
it('falls back to #000000 when no default is declared', () => {
|
||||
const node = createMockNode()
|
||||
const widget = useColorWidget()(node, {
|
||||
type: 'COLOR',
|
||||
name: 'color'
|
||||
} as InputSpec)
|
||||
expect(widget.value).toBe('#000000')
|
||||
it('honours a custom input name from the spec', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
|
||||
useColorWidget()(
|
||||
node,
|
||||
createColorSpec({ name: 'bg_color', default: DECLARED_DEFAULT })
|
||||
)
|
||||
|
||||
expect(addWidget.mock.calls[0]![1]).toBe('bg_color')
|
||||
})
|
||||
|
||||
it('returns the existing widget instead of creating a duplicate', () => {
|
||||
const node = createMockNode()
|
||||
const first = useColorWidget()(node, colorSpec)
|
||||
const second = useColorWidget()(node, colorSpec)
|
||||
const { node, addWidget } = createMockNode()
|
||||
const inputSpec = createColorSpec({ default: DECLARED_DEFAULT })
|
||||
|
||||
const first = useColorWidget()(node, inputSpec)
|
||||
const second = useColorWidget()(node, inputSpec)
|
||||
|
||||
expect(second).toBe(first)
|
||||
expect(addWidget).toHaveBeenCalledOnce()
|
||||
expect(node.widgets).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('throws when the input spec is not a color spec', () => {
|
||||
const { node } = createMockNode()
|
||||
const inputSpec = { type: 'STRING', name: 'color' } as unknown as InputSpec
|
||||
|
||||
expect(() => useColorWidget()(node, inputSpec)).toThrow(
|
||||
'Invalid input spec for color widget'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IColorWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type {
|
||||
ColorInputSpec,
|
||||
InputSpec as InputSpecV2
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { isColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
export const useColorWidget = (): ComfyWidgetConstructorV2 => {
|
||||
return (node: LGraphNode, inputSpec: InputSpecV2): IColorWidget => {
|
||||
const colorSpec = inputSpec as ColorInputSpec
|
||||
const { name, options } = colorSpec
|
||||
const defaultValue = colorSpec.default ?? options?.default ?? '#000000'
|
||||
if (!isColorInputSpec(inputSpec)) {
|
||||
throw new Error('Invalid input spec for color widget')
|
||||
}
|
||||
|
||||
const { name, default: defaultValue = '#000000' } = inputSpec
|
||||
|
||||
const existing = node.widgets?.find(
|
||||
(w): w is IColorWidget => w.name === name && w.type === 'color'
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useGalleriaWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useGalleriaWidget'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
function createMockNode(): {
|
||||
node: LGraphNode
|
||||
addWidget: ReturnType<typeof vi.spyOn>
|
||||
} {
|
||||
const node = new LGraphNode('TestGalleriaNode')
|
||||
const addWidget = vi
|
||||
.spyOn(node, 'addWidget')
|
||||
.mockImplementation((type, name, value, _callback, options) => {
|
||||
const widget = {
|
||||
type,
|
||||
name,
|
||||
value,
|
||||
options: typeof options === 'string' ? { property: options } : options,
|
||||
y: 0
|
||||
} as IBaseWidget
|
||||
node.widgets ??= []
|
||||
node.widgets.push(widget)
|
||||
return widget
|
||||
})
|
||||
return { node, addWidget }
|
||||
}
|
||||
|
||||
function createGalleriaSpec(images?: string[]): InputSpec {
|
||||
return { type: 'GALLERIA', name: 'gallery', images } as InputSpec
|
||||
}
|
||||
|
||||
describe('useGalleriaWidget', () => {
|
||||
it('uses the declared images from the input spec', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
const images = ['a.png', 'b.png']
|
||||
|
||||
const widget = useGalleriaWidget()(node, createGalleriaSpec(images))
|
||||
|
||||
expect(addWidget).toHaveBeenCalledWith(
|
||||
'galleria',
|
||||
'gallery',
|
||||
images,
|
||||
expect.any(Function),
|
||||
{ serialize: true }
|
||||
)
|
||||
expect(widget.value).toBe(images)
|
||||
})
|
||||
|
||||
it('defaults to an empty image list', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
|
||||
const widget = useGalleriaWidget()(node, createGalleriaSpec())
|
||||
|
||||
expect(addWidget).toHaveBeenCalledWith(
|
||||
'galleria',
|
||||
'gallery',
|
||||
[],
|
||||
expect.any(Function),
|
||||
{ serialize: true }
|
||||
)
|
||||
expect(widget.type).toBe('galleria')
|
||||
})
|
||||
|
||||
it('throws when the input spec is not a galleria spec', () => {
|
||||
const { node } = createMockNode()
|
||||
const inputSpec = {
|
||||
type: 'STRING',
|
||||
name: 'gallery'
|
||||
} as unknown as InputSpec
|
||||
|
||||
expect(() => useGalleriaWidget()(node, inputSpec)).toThrow(
|
||||
'Invalid input spec for galleria widget'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,25 +1,20 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IGalleriaWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type {
|
||||
GalleriaInputSpec,
|
||||
InputSpec as InputSpecV2
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { isGalleriaInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
export const useGalleriaWidget = (): ComfyWidgetConstructorV2 => {
|
||||
return (node: LGraphNode, inputSpec: InputSpecV2): IGalleriaWidget => {
|
||||
const { name, options = {} } = inputSpec as GalleriaInputSpec
|
||||
if (!isGalleriaInputSpec(inputSpec)) {
|
||||
throw new Error('Invalid input spec for galleria widget')
|
||||
}
|
||||
|
||||
const widget = node.addWidget(
|
||||
'galleria',
|
||||
name,
|
||||
options.images || [],
|
||||
() => {},
|
||||
{
|
||||
serialize: true,
|
||||
...options
|
||||
}
|
||||
) as IGalleriaWidget
|
||||
const { name, images = [] } = inputSpec
|
||||
|
||||
const widget = node.addWidget('galleria', name, images, () => {}, {
|
||||
serialize: true
|
||||
}) as IGalleriaWidget
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useTextareaWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useTextareaWidget'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
function createMockNode(): {
|
||||
node: LGraphNode
|
||||
addWidget: ReturnType<typeof vi.spyOn>
|
||||
} {
|
||||
const node = new LGraphNode('TestTextareaNode')
|
||||
const addWidget = vi
|
||||
.spyOn(node, 'addWidget')
|
||||
.mockImplementation((type, name, value, _callback, options) => {
|
||||
const widget = {
|
||||
type,
|
||||
name,
|
||||
value,
|
||||
options: typeof options === 'string' ? { property: options } : options,
|
||||
y: 0
|
||||
} as IBaseWidget
|
||||
node.widgets ??= []
|
||||
node.widgets.push(widget)
|
||||
return widget
|
||||
})
|
||||
return { node, addWidget }
|
||||
}
|
||||
|
||||
function createTextareaSpec(
|
||||
overrides: Partial<{
|
||||
default: string
|
||||
rows: number
|
||||
cols: number
|
||||
}> = {}
|
||||
): InputSpec {
|
||||
return { type: 'TEXTAREA', name: 'text', ...overrides } as InputSpec
|
||||
}
|
||||
|
||||
describe('useTextareaWidget', () => {
|
||||
it('uses the declared default, rows, and cols from the input spec', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
|
||||
const widget = useTextareaWidget()(
|
||||
node,
|
||||
createTextareaSpec({ default: 'hello', rows: 10, cols: 80 })
|
||||
)
|
||||
|
||||
expect(addWidget).toHaveBeenCalledWith(
|
||||
'textarea',
|
||||
'text',
|
||||
'hello',
|
||||
expect.any(Function),
|
||||
{ serialize: true, rows: 10, cols: 80 }
|
||||
)
|
||||
expect(widget.value).toBe('hello')
|
||||
})
|
||||
|
||||
it('falls back to an empty default with 5 rows and 50 cols', () => {
|
||||
const { node, addWidget } = createMockNode()
|
||||
|
||||
const widget = useTextareaWidget()(node, createTextareaSpec())
|
||||
|
||||
expect(addWidget).toHaveBeenCalledWith(
|
||||
'textarea',
|
||||
'text',
|
||||
'',
|
||||
expect.any(Function),
|
||||
{ serialize: true, rows: 5, cols: 50 }
|
||||
)
|
||||
expect(widget.type).toBe('textarea')
|
||||
})
|
||||
|
||||
it('throws when the input spec is not a textarea spec', () => {
|
||||
const { node } = createMockNode()
|
||||
const inputSpec = { type: 'STRING', name: 'text' } as unknown as InputSpec
|
||||
|
||||
expect(() => useTextareaWidget()(node, inputSpec)).toThrow(
|
||||
'Invalid input spec for textarea widget'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,27 +1,23 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ITextareaWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type {
|
||||
InputSpec as InputSpecV2,
|
||||
TextareaInputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { isTextareaInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
export const useTextareaWidget = (): ComfyWidgetConstructorV2 => {
|
||||
return (node: LGraphNode, inputSpec: InputSpecV2): ITextareaWidget => {
|
||||
const { name, options = {} } = inputSpec as TextareaInputSpec
|
||||
if (!isTextareaInputSpec(inputSpec)) {
|
||||
throw new Error('Invalid input spec for textarea widget')
|
||||
}
|
||||
|
||||
const widget = node.addWidget(
|
||||
'textarea',
|
||||
name,
|
||||
options.default || '',
|
||||
() => {},
|
||||
{
|
||||
serialize: true,
|
||||
rows: options.rows || 5,
|
||||
cols: options.cols || 50,
|
||||
...options
|
||||
}
|
||||
) as ITextareaWidget
|
||||
const { name, default: defaultValue = '', rows = 5, cols = 50 } = inputSpec
|
||||
|
||||
const widgetOptions = { rows, cols }
|
||||
|
||||
const widget = node.addWidget('textarea', name, defaultValue, () => {}, {
|
||||
serialize: true,
|
||||
...widgetOptions
|
||||
}) as ITextareaWidget
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
@@ -199,6 +199,32 @@ describe('NodeDef Migration', () => {
|
||||
expect(result.inputs['customInput'].default).toBe('custom value')
|
||||
})
|
||||
|
||||
it('should map CHART option type to chartType', () => {
|
||||
const plainObject = {
|
||||
optional: {
|
||||
chartInput: ['CHART', { type: 'bar', data: { labels: [] } }]
|
||||
}
|
||||
} as ComfyNodeDefV1['input']
|
||||
|
||||
const nodeDef: ComfyNodeDefV1 = {
|
||||
name: 'TestNode',
|
||||
display_name: 'Test Node',
|
||||
category: 'Testing',
|
||||
python_module: 'test_module',
|
||||
description: 'A test node',
|
||||
input: plainObject,
|
||||
output: [],
|
||||
output_is_list: [],
|
||||
output_name: [],
|
||||
output_node: false
|
||||
}
|
||||
|
||||
const result = transformNodeDefV1ToV2(nodeDef)
|
||||
const chartInput = result.inputs['chartInput']
|
||||
expect(chartInput.type).toBe('CHART')
|
||||
expect(chartInput).toMatchObject({ chartType: 'bar', data: { labels: [] } })
|
||||
})
|
||||
|
||||
it('should not transform hidden fields', () => {
|
||||
const plainObject = {
|
||||
hidden: {
|
||||
|
||||
@@ -121,6 +121,17 @@ export function transformInputSpecV1ToV2(
|
||||
? inputSpecV1[0]
|
||||
: getComboSpecComboOptions(inputSpecV1)
|
||||
}
|
||||
} else if (inputSpecV1[0] === 'CHART') {
|
||||
// CHART uses `chartType` in V2 because `type` is the discriminator key
|
||||
const { type, ...chartOptions } = options
|
||||
const chartType = type === 'bar' || type === 'line' ? type : undefined
|
||||
return {
|
||||
type: 'CHART',
|
||||
name,
|
||||
isOptional,
|
||||
...chartOptions,
|
||||
chartType
|
||||
}
|
||||
} else if (typeof inputSpecV1[0] === 'string') {
|
||||
// Handle standard types (INT, FLOAT, BOOLEAN, STRING) and custom types
|
||||
return {
|
||||
|
||||
@@ -44,11 +44,7 @@ const zColorInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('COLOR'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
default: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
default: z.string().optional()
|
||||
})
|
||||
|
||||
const zImageInputSpec = zBaseInputOptions.extend({
|
||||
@@ -84,34 +80,22 @@ const zMarkdownInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('MARKDOWN'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
content: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
default: z.string().optional()
|
||||
})
|
||||
|
||||
const zChartInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('CHART'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
type: z.enum(['bar', 'line']).optional(),
|
||||
data: z.object({}).optional()
|
||||
})
|
||||
.optional()
|
||||
chartType: z.enum(['bar', 'line']).optional(),
|
||||
data: z.object({}).optional()
|
||||
})
|
||||
|
||||
const zGalleriaInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('GALLERIA'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
images: z.array(z.string()).optional()
|
||||
})
|
||||
.optional()
|
||||
images: z.array(z.string()).optional()
|
||||
})
|
||||
|
||||
const zColorsInputSpec = zBaseInputOptions.extend({
|
||||
@@ -147,13 +131,9 @@ const zTextareaInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('TEXTAREA'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
rows: z.number().optional(),
|
||||
cols: z.number().optional(),
|
||||
default: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
rows: z.number().optional(),
|
||||
cols: z.number().optional(),
|
||||
default: z.string().optional()
|
||||
})
|
||||
|
||||
const zCurvePoint = z.tuple([z.number(), z.number()])
|
||||
@@ -302,3 +282,21 @@ export const isChartInputSpec = (
|
||||
): inputSpec is ChartInputSpec => {
|
||||
return inputSpec.type === 'CHART'
|
||||
}
|
||||
|
||||
export const isColorInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is ColorInputSpec => {
|
||||
return inputSpec.type === 'COLOR'
|
||||
}
|
||||
|
||||
export const isTextareaInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is TextareaInputSpec => {
|
||||
return inputSpec.type === 'TEXTAREA'
|
||||
}
|
||||
|
||||
export const isGalleriaInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is GalleriaInputSpec => {
|
||||
return inputSpec.type === 'GALLERIA'
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ from .nodes import (
|
||||
LongComboDropdown,
|
||||
MultiSelectNode,
|
||||
NodeWithBooleanInput,
|
||||
NodeWithColorInput,
|
||||
NodeWithDefaultInput,
|
||||
NodeWithForceInput,
|
||||
NodeWithOptionalComboInput,
|
||||
@@ -43,6 +44,7 @@ __all__ = [
|
||||
"LongComboDropdown",
|
||||
"MultiSelectNode",
|
||||
"NodeWithBooleanInput",
|
||||
"NodeWithColorInput",
|
||||
"NodeWithDefaultInput",
|
||||
"NodeWithForceInput",
|
||||
"NodeWithOptionalComboInput",
|
||||
|
||||
@@ -11,6 +11,7 @@ from .errors import (
|
||||
from .inputs import (
|
||||
LongComboDropdown,
|
||||
NodeWithBooleanInput,
|
||||
NodeWithColorInput,
|
||||
NodeWithDefaultInput,
|
||||
NodeWithForceInput,
|
||||
NodeWithOptionalComboInput,
|
||||
@@ -69,6 +70,7 @@ __all__ = [
|
||||
"LongComboDropdown",
|
||||
"MultiSelectNode",
|
||||
"NodeWithBooleanInput",
|
||||
"NodeWithColorInput",
|
||||
"NodeWithDefaultInput",
|
||||
"NodeWithForceInput",
|
||||
"NodeWithOptionalComboInput",
|
||||
|
||||
@@ -217,6 +217,20 @@ class NodeWithBooleanInput:
|
||||
print(f"boolean_input: {boolean_input}")
|
||||
|
||||
|
||||
class NodeWithColorInput:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"color_input": ("COLOR", {"default": "#00ff00"})}}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "node_with_color_input"
|
||||
CATEGORY = "DevTools"
|
||||
DESCRIPTION = "A node with a color input that declares a non-black default"
|
||||
|
||||
def node_with_color_input(self, color_input: str):
|
||||
print(f"color_input: {color_input}")
|
||||
|
||||
|
||||
class SimpleSlider:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -379,6 +393,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"DevToolsNodeWithStringInput": NodeWithStringInput,
|
||||
"DevToolsNodeWithUnionInput": NodeWithUnionInput,
|
||||
"DevToolsNodeWithBooleanInput": NodeWithBooleanInput,
|
||||
"DevToolsNodeWithColorInput": NodeWithColorInput,
|
||||
"DevToolsSimpleSlider": SimpleSlider,
|
||||
"DevToolsNodeWithSeedInput": NodeWithSeedInput,
|
||||
"DevToolsNodeWithValidation": NodeWithValidation,
|
||||
@@ -399,6 +414,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DevToolsNodeWithStringInput": "Node With String Input",
|
||||
"DevToolsNodeWithUnionInput": "Node With Union Input",
|
||||
"DevToolsNodeWithBooleanInput": "Node With Boolean Input",
|
||||
"DevToolsNodeWithColorInput": "Node With Color Input",
|
||||
"DevToolsSimpleSlider": "Simple Slider",
|
||||
"DevToolsNodeWithSeedInput": "Node With Seed Input",
|
||||
"DevToolsNodeWithValidation": "Node With Validation",
|
||||
@@ -419,6 +435,7 @@ __all__ = [
|
||||
"NodeWithStringInput",
|
||||
"NodeWithUnionInput",
|
||||
"NodeWithBooleanInput",
|
||||
"NodeWithColorInput",
|
||||
"SimpleSlider",
|
||||
"NodeWithSeedInput",
|
||||
"NodeWithValidation",
|
||||
|
||||
Reference in New Issue
Block a user