mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary Promoted primitive subgraph inputs (String, Int) render their link anchor at the header position instead of the widget row. Renaming subgraph input labels breaks the match entirely, causing connections to detach from their widgets visually. ## Changes - **What**: Fix widget-input slot positioning for promoted subgraph inputs in both LiteGraph and Vue (Nodes 2.0) rendering modes - `_arrangeWidgetInputSlots`: Removed Vue mode branch that skipped setting `input.pos`. Promoted widget inputs aren't rendered as `<InputSlot>` Vue components (NodeSlots filters them out), so `input.pos` is the only position fallback - `drawConnections`: Added pre-pass to arrange nodes with unpositioned widget-input slots before link rendering. The background canvas renders before the foreground canvas calls `arrange()`, so positions weren't set on the first frame - `SubgraphNode`: Sync `input.widget.name` with the display name on label rename and initial setup. The `IWidgetLocator` name diverged from `PromotedWidgetView.name` after rename, breaking all name-based slot↔widget matching (`_arrangeWidgetInputSlots`, `getWidgetFromSlot`, `getSlotFromWidget`) ## Review Focus - The `_arrangeWidgetInputSlots` rewrite iterates `_concreteInputs` directly instead of building a spread-copy map — simpler and avoids the stale index issue - `input.widget.name` is now kept in sync with the display name (`input.label ?? subgraphInput.name`). This is a semantic shift from using the raw internal name, but it's required for all name-based matching to work after renames. The value is overwritten on deserialize by `_setWidget` anyway - The `_widget` fallback in `_arrangeWidgetInputSlots` is a safety net for edge cases where the name still doesn't match (e.g., stale cache) Fixes #9998 ## Screenshots <img width="847" height="476" alt="Screenshot 2026-03-17 at 3 05 32 PM" src="https://github.com/user-attachments/assets/38f10563-f0bc-44dd-a1a5-f4a7832575d0" /> <img width="804" height="471" alt="Screenshot 2026-03-17 at 3 05 23 PM" src="https://github.com/user-attachments/assets/3237a7ee-f3e5-4084-b330-371def3415bd" /> <img width="974" height="571" alt="Screenshot 2026-03-17 at 3 05 16 PM" src="https://github.com/user-attachments/assets/cafdca46-8d9b-40e1-8561-02cbb25ee8f2" /> <img width="967" height="558" alt="Screenshot 2026-03-17 at 3 05 06 PM" src="https://github.com/user-attachments/assets/fc03ce43-906c-474d-b3bc-ddf08eb37c75" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10195-fix-subgraph-promoted-widget-input-slot-positions-after-label-rename-3266d73d365081dfa623dd94dd87c718) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: jaeone94 <jaeone.prt@gmail.com>
118 lines
4.4 KiB
TypeScript
118 lines
4.4 KiB
TypeScript
import {
|
|
comfyPageFixture as test,
|
|
comfyExpect as expect
|
|
} from '../fixtures/ComfyPage'
|
|
|
|
const WORKFLOW = 'subgraphs/test-values-input-subgraph'
|
|
const RENAMED_LABEL = 'my_seed'
|
|
|
|
/**
|
|
* Regression test for subgraph input slot rename propagation.
|
|
*
|
|
* Renaming a SubgraphInput slot (e.g. "seed") inside the subgraph must
|
|
* update the promoted widget label shown on the parent SubgraphNode and
|
|
* keep the widget positioned in the node body (not the header).
|
|
*
|
|
* See: https://github.com/Comfy-Org/ComfyUI_frontend/pull/10195
|
|
*/
|
|
test.describe(
|
|
'Subgraph input slot rename propagation',
|
|
{ tag: ['@subgraph', '@widget'] },
|
|
() => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
|
})
|
|
|
|
test('Renaming a subgraph input slot updates the widget label on the parent node', async ({
|
|
comfyPage
|
|
}) => {
|
|
const { page } = comfyPage
|
|
|
|
// 1. Load workflow with subgraph containing a promoted seed widget input
|
|
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
|
await comfyPage.vueNodes.waitForNodes()
|
|
|
|
const sgNode = comfyPage.vueNodes.getNodeLocator('19')
|
|
await expect(sgNode).toBeVisible()
|
|
|
|
// 2. Verify the seed widget is visible on the parent node
|
|
const seedWidget = sgNode.getByLabel('seed', { exact: true })
|
|
await expect(seedWidget).toBeVisible()
|
|
|
|
// Verify widget is in the node body, not the header
|
|
const headerBox = await sgNode
|
|
.locator('[data-testid^="node-header-"]')
|
|
.boundingBox()
|
|
const widgetBox = await seedWidget.boundingBox()
|
|
expect(headerBox).not.toBeNull()
|
|
expect(widgetBox).not.toBeNull()
|
|
expect(widgetBox!.y).toBeGreaterThan(headerBox!.y + headerBox!.height)
|
|
|
|
// 3. Enter the subgraph and rename the seed slot.
|
|
// The subgraph IO rename uses canvas.prompt() which requires the
|
|
// litegraph context menu, so temporarily disable Vue nodes.
|
|
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
|
|
await comfyPage.nextFrame()
|
|
|
|
const sgNodeRef = await comfyPage.nodeOps.getNodeRefById('19')
|
|
await sgNodeRef.navigateIntoSubgraph()
|
|
|
|
// Find the seed SubgraphInput slot
|
|
const seedSlotName = await page.evaluate(() => {
|
|
const graph = window.app!.canvas.graph
|
|
if (!graph) return null
|
|
const inputs = (
|
|
graph as { inputs?: Array<{ name: string; type: string }> }
|
|
).inputs
|
|
return inputs?.find((i) => i.name.includes('seed'))?.name ?? null
|
|
})
|
|
expect(seedSlotName).not.toBeNull()
|
|
|
|
// 4. Right-click the seed input slot and rename it
|
|
await comfyPage.subgraph.rightClickInputSlot(seedSlotName!)
|
|
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
|
|
await comfyPage.nextFrame()
|
|
|
|
const dialog = '.graphdialog input'
|
|
await page.waitForSelector(dialog, { state: 'visible' })
|
|
await page.fill(dialog, '')
|
|
await page.fill(dialog, RENAMED_LABEL)
|
|
await page.keyboard.press('Enter')
|
|
await page.waitForSelector(dialog, { state: 'hidden' })
|
|
|
|
// 5. Navigate back to parent graph and re-enable Vue nodes
|
|
await comfyPage.subgraph.exitViaBreadcrumb()
|
|
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
|
await comfyPage.vueNodes.waitForNodes()
|
|
|
|
// 6. Verify the widget label updated to the renamed value
|
|
const sgNodeAfter = comfyPage.vueNodes.getNodeLocator('19')
|
|
await expect(sgNodeAfter).toBeVisible()
|
|
|
|
const updatedLabel = await page.evaluate(() => {
|
|
const node = window.app!.canvas.graph!.getNodeById('19')
|
|
if (!node) return null
|
|
const w = node.widgets?.find((w: { name: string }) =>
|
|
w.name.includes('seed')
|
|
)
|
|
return w?.label || w?.name || null
|
|
})
|
|
expect(updatedLabel).toBe(RENAMED_LABEL)
|
|
|
|
// 7. Verify the widget is still in the body, not the header
|
|
const seedWidgetAfter = sgNodeAfter.getByLabel('seed', { exact: true })
|
|
await expect(seedWidgetAfter).toBeVisible()
|
|
|
|
const headerAfter = await sgNodeAfter
|
|
.locator('[data-testid^="node-header-"]')
|
|
.boundingBox()
|
|
const widgetAfter = await seedWidgetAfter.boundingBox()
|
|
expect(headerAfter).not.toBeNull()
|
|
expect(widgetAfter).not.toBeNull()
|
|
expect(widgetAfter!.y).toBeGreaterThan(
|
|
headerAfter!.y + headerAfter!.height
|
|
)
|
|
})
|
|
}
|
|
)
|