mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-23 14:16:00 +00:00
Compare commits
5 Commits
v1.45.14
...
glary/widg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68fdfd5e35 | ||
|
|
5841c252ce | ||
|
|
4d4ad6ed92 | ||
|
|
86b6cab5e9 | ||
|
|
0aefef7c42 |
31
browser_tests/assets/widgets/combo_control_widget.json
Normal file
31
browser_tests/assets/widgets/combo_control_widget.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"last_node_id": 1,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "DevToolsNodeWithComboControlWidget",
|
||||
"pos": [20, 50],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "DevToolsNodeWithComboControlWidget"
|
||||
},
|
||||
"widgets_values": ["Option A", "fixed", ""]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -498,6 +498,25 @@ export class SubgraphHelper {
|
||||
await this.comfyPage.contextMenu.waitForHidden()
|
||||
}
|
||||
|
||||
async getInnerControlWidgetLabels(): Promise<string[]> {
|
||||
return this.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
const subgraphNode = graph.nodes.find(
|
||||
(n: { isSubgraphNode?: () => boolean }) =>
|
||||
typeof n.isSubgraphNode === 'function' && n.isSubgraphNode()
|
||||
) as { subgraph?: Subgraph } | undefined
|
||||
if (!subgraphNode?.subgraph) return []
|
||||
const innerNodes = Array.from(subgraphNode.subgraph.nodes.values())
|
||||
return innerNodes.flatMap((n: { widgets?: Array<{ label?: string }> }) =>
|
||||
(n.widgets ?? [])
|
||||
.filter((w: { label?: string }) =>
|
||||
(w.label ?? '').includes('control')
|
||||
)
|
||||
.map((w: { label?: string }) => w.label!)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async findSubgraphNodeId(): Promise<string> {
|
||||
const id = await this.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
|
||||
251
browser_tests/tests/numberControlWidget.spec.ts
Normal file
251
browser_tests/tests/numberControlWidget.spec.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
test('Can drag adjust value', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('widgets/seed_widget')
|
||||
|
||||
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
|
||||
const widget = await node.getWidget(0)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window.widgetValue = undefined
|
||||
const widget = window.app!.graph!.nodes[0].widgets![0]
|
||||
widget.callback = (value: number) => {
|
||||
window.widgetValue = value
|
||||
}
|
||||
})
|
||||
await widget.dragHorizontal(50)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('seed_widget_dragged.png')
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.page.evaluate(() => window.widgetValue))
|
||||
.toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('WidgetControlMode setting', { tag: '@widget' }, () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'after')
|
||||
})
|
||||
|
||||
test('Changing mode to "before" updates control widget labels', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'after')
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
const ksampler = (await comfyPage.nodeOps.getNodeRefsByType('KSampler'))[0]
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate((id) => {
|
||||
const node = window.app!.graph!.getNodeById(id)
|
||||
return node?.widgets
|
||||
?.filter((w) => (w.label ?? '').includes('control'))
|
||||
.map((w) => w.label)
|
||||
}, ksampler.id)
|
||||
)
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining('after')]))
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'before')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate((id) => {
|
||||
const node = window.app!.graph!.getNodeById(id)
|
||||
return node?.widgets
|
||||
?.filter((w) => (w.label ?? '').includes('control'))
|
||||
.map((w) => w.label)
|
||||
}, ksampler.id)
|
||||
)
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining('before')]))
|
||||
})
|
||||
|
||||
test('Changing mode back to "after" restores labels', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'before')
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
const ksampler = (await comfyPage.nodeOps.getNodeRefsByType('KSampler'))[0]
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'after')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate((id) => {
|
||||
const node = window.app!.graph!.getNodeById(id)
|
||||
return node?.widgets
|
||||
?.filter((w) => (w.label ?? '').includes('control'))
|
||||
.map((w) => w.label)
|
||||
}, ksampler.id)
|
||||
)
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining('after')]))
|
||||
})
|
||||
|
||||
test('Mode change updates control widgets across multiple nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.LiteGraph!.createNode('KSampler')
|
||||
node!.pos = [400, 30]
|
||||
window.app!.graph!.add(node!)
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'before')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate(() => {
|
||||
const ksamplers = window.app!.graph!.nodes.filter(
|
||||
(n) => n.type === 'KSampler'
|
||||
)
|
||||
return (
|
||||
ksamplers.length === 2 &&
|
||||
ksamplers.every((n) => {
|
||||
const controlLabels = (n.widgets ?? [])
|
||||
.filter((w) => (w.label ?? '').includes('control'))
|
||||
.map((w) => w.label ?? '')
|
||||
return (
|
||||
controlLabels.length > 0 &&
|
||||
controlLabels.every((label) => label.includes('before'))
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
test('Nodes without widgets are skipped without error', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.LiteGraph!.createNode('Reroute')
|
||||
if (node) {
|
||||
node.pos = [400, 30]
|
||||
window.app!.graph!.add(node)
|
||||
}
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'before')
|
||||
|
||||
const ksampler = (await comfyPage.nodeOps.getNodeRefsByType('KSampler'))[0]
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate((id) => {
|
||||
const node = window.app!.graph!.getNodeById(id)
|
||||
return node?.widgets
|
||||
?.filter((w) => (w.label ?? '').includes('control'))
|
||||
.map((w) => w.label)
|
||||
}, ksampler.id)
|
||||
)
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining('before')]))
|
||||
})
|
||||
|
||||
test('Canvas is marked dirty after mode change', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const w = window as Window & { __canvasDirtied?: boolean }
|
||||
w.__canvasDirtied = false
|
||||
const origSetDirty = window.app!.canvas.setDirty.bind(window.app!.canvas)
|
||||
window.app!.canvas.setDirty = (
|
||||
...args: Parameters<typeof origSetDirty>
|
||||
) => {
|
||||
w.__canvasDirtied = true
|
||||
return origSetDirty(...args)
|
||||
}
|
||||
})
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'before')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate(
|
||||
() =>
|
||||
(window as Window & { __canvasDirtied?: boolean }).__canvasDirtied
|
||||
)
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
test('Mode change updates combo control widget labels', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'after')
|
||||
await comfyPage.workflow.loadWorkflow('widgets/combo_control_widget')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph!.nodes[0]
|
||||
return (node?.widgets ?? [])
|
||||
.filter((w) => (w.label ?? '').includes('control'))
|
||||
.map((w) => w.label!)
|
||||
})
|
||||
)
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining('after')]))
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'before')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph!.nodes[0]
|
||||
return (node?.widgets ?? [])
|
||||
.filter((w) => (w.label ?? '').includes('control'))
|
||||
.map((w) => w.label!)
|
||||
})
|
||||
)
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining('before')]))
|
||||
})
|
||||
|
||||
test('Mode change propagates to linkedWidgets on control widgets', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// linkedWidgets is only set on main widgets, never on control widgets
|
||||
// themselves. This covers the defensive code path (GraphCanvas.vue:360-362).
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph!.nodes[0]
|
||||
if (!node?.widgets) return
|
||||
const controlWidget = node.widgets.find((w) =>
|
||||
(w.label ?? '').includes('control')
|
||||
)
|
||||
if (!controlWidget) return
|
||||
const mockLinked = Object.create(null)
|
||||
mockLinked.name = 'mock_filter'
|
||||
mockLinked.label = 'control after generate'
|
||||
mockLinked.type = 'string'
|
||||
mockLinked.value = ''
|
||||
controlWidget.linkedWidgets = [mockLinked]
|
||||
})
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'before')
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph!.nodes[0]
|
||||
const controlWidget = node?.widgets?.find((w) =>
|
||||
(w.label ?? '').includes('control')
|
||||
)
|
||||
const linked = controlWidget?.linkedWidgets ?? []
|
||||
return [controlWidget?.label, ...linked.map((l) => l.label ?? '')]
|
||||
})
|
||||
)
|
||||
.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('before'),
|
||||
expect.stringContaining('before')
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -608,6 +608,33 @@ test.describe(
|
||||
}
|
||||
)
|
||||
|
||||
test.describe(
|
||||
'WidgetControlMode in subgraphs',
|
||||
{ tag: ['@subgraph', '@widget'] },
|
||||
() => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'after')
|
||||
})
|
||||
|
||||
test('Mode change updates control widget labels inside subgraph nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'after')
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.subgraph.getInnerControlWidgetLabels())
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining('after')]))
|
||||
|
||||
await comfyPage.settings.setSetting('Comfy.WidgetControlMode', 'before')
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.subgraph.getInnerControlWidgetLabels())
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining('before')]))
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test('Promote/Demote by Context Menu @vue-nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
|
||||
const ksampler = comfyPage.vueNodes.getNodeLocator('1')
|
||||
|
||||
@@ -137,28 +137,6 @@ test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
|
||||
test('Can drag adjust value', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('widgets/seed_widget')
|
||||
|
||||
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
|
||||
const widget = await node.getWidget(0)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window.widgetValue = undefined
|
||||
const widget = window.app!.graph!.nodes[0].widgets![0]
|
||||
widget.callback = (value: number) => {
|
||||
window.widgetValue = value
|
||||
}
|
||||
})
|
||||
await widget.dragHorizontal(50)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('seed_widget_dragged.png')
|
||||
|
||||
await expect
|
||||
.poll(() => comfyPage.page.evaluate(() => window.widgetValue))
|
||||
.toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe(
|
||||
'Dynamic widget manipulation',
|
||||
{ tag: ['@screenshot', '@widget'] },
|
||||
|
||||
@@ -10,6 +10,7 @@ from .nodes import (
|
||||
LongComboDropdown,
|
||||
MultiSelectNode,
|
||||
NodeWithBooleanInput,
|
||||
NodeWithComboControlWidget,
|
||||
NodeWithDefaultInput,
|
||||
NodeWithForceInput,
|
||||
NodeWithOptionalComboInput,
|
||||
@@ -43,6 +44,7 @@ __all__ = [
|
||||
"LongComboDropdown",
|
||||
"MultiSelectNode",
|
||||
"NodeWithBooleanInput",
|
||||
"NodeWithComboControlWidget",
|
||||
"NodeWithDefaultInput",
|
||||
"NodeWithForceInput",
|
||||
"NodeWithOptionalComboInput",
|
||||
|
||||
@@ -11,6 +11,7 @@ from .errors import (
|
||||
from .inputs import (
|
||||
LongComboDropdown,
|
||||
NodeWithBooleanInput,
|
||||
NodeWithComboControlWidget,
|
||||
NodeWithDefaultInput,
|
||||
NodeWithForceInput,
|
||||
NodeWithOptionalComboInput,
|
||||
@@ -69,6 +70,7 @@ __all__ = [
|
||||
"LongComboDropdown",
|
||||
"MultiSelectNode",
|
||||
"NodeWithBooleanInput",
|
||||
"NodeWithComboControlWidget",
|
||||
"NodeWithDefaultInput",
|
||||
"NodeWithForceInput",
|
||||
"NodeWithOptionalComboInput",
|
||||
|
||||
@@ -344,6 +344,31 @@ class NodeWithPriceBadge(IO.ComfyNode):
|
||||
return IO.NodeOutput()
|
||||
|
||||
|
||||
class NodeWithComboControlWidget:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"combo_option": (
|
||||
"COMBO",
|
||||
{
|
||||
"options": ["Option A", "Option B", "Option C"],
|
||||
"control_after_generate": True,
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "DevTools"
|
||||
DESCRIPTION = "A node with a combo input that has control_after_generate, producing control widgets with a filter list"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def execute(self, combo_option: str):
|
||||
return (combo_option,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DevToolsLongComboDropdown": LongComboDropdown,
|
||||
"DevToolsNodeWithOptionalInput": NodeWithOptionalInput,
|
||||
@@ -359,6 +384,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"DevToolsNodeWithSeedInput": NodeWithSeedInput,
|
||||
"DevToolsNodeWithValidation": NodeWithValidation,
|
||||
"DevToolsNodeWithV2ComboInput": NodeWithV2ComboInput,
|
||||
"DevToolsNodeWithComboControlWidget": NodeWithComboControlWidget,
|
||||
"DevToolsNodeWithLegacyWidget": NodeWithLegacyWidget,
|
||||
"DevToolsNodeWithPriceBadge": NodeWithPriceBadge,
|
||||
}
|
||||
@@ -378,6 +404,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DevToolsNodeWithSeedInput": "Node With Seed Input",
|
||||
"DevToolsNodeWithValidation": "Node With Validation",
|
||||
"DevToolsNodeWithV2ComboInput": "Node With V2 Combo Input",
|
||||
"DevToolsNodeWithComboControlWidget": "Node With Combo Control Widget",
|
||||
"DevToolsNodeWithLegacyWidget": "Node With Legacy Widget",
|
||||
"DevToolsNodeWithPriceBadge": "Node With Price Badge",
|
||||
}
|
||||
@@ -397,6 +424,7 @@ __all__ = [
|
||||
"NodeWithSeedInput",
|
||||
"NodeWithValidation",
|
||||
"NodeWithV2ComboInput",
|
||||
"NodeWithComboControlWidget",
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user