Files
ComfyUI_frontend/browser_tests/tests/collapsedNodeLinks.spec.ts
jaeone94 394e36984f fix: re-sync collapsed node slot positions after subgraph fitView (#11240)
## Summary

Fix collapsed node connection links rendering at wrong positions when
entering a subgraph for the first time. `fitView()` (added in #10995)
changes canvas scale/offset, invalidating cached slot positions for
collapsed nodes.

## Changes

- **What**: Schedule `requestSlotLayoutSyncForAllNodes()` on the next
frame after `fitView()` in `restoreViewport()` so collapsed node slot
positions are re-measured against the updated transform. Inner RAF
guarded against mid-frame graph changes.
- **Test coverage**:
- Unit tests in `subgraphNavigationStore.viewport.test.ts` verify the
RAF chain calls `requestSlotLayoutSyncForAllNodes` after `fitView`, and
skip the re-sync when the active graph changes between frames.
- E2E screenshot test (`@screenshot` tag) validates correct link
rendering on first subgraph entry using a new fixture with a
pre-collapsed inner node.

## Review Focus

The nested `requestAnimationFrame` is intentional: the outer RAF runs
`fitView()`, which updates `ds.scale`/`ds.offset` and triggers a CSS
transform update on `TransformPane`. The inner RAF ensures the DOM has
reflowed with the new transform before
`requestSlotLayoutSyncForAllNodes()` measures `getBoundingClientRect()`
on slot elements.

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-04-16 12:38:01 +00:00

93 lines
2.9 KiB
TypeScript

import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { assertNodeSlotsWithinBounds } from '@e2e/fixtures/utils/slotBoundsUtil'
const NODE_ID = '3'
const NODE_TITLE = 'KSampler'
test.describe(
'Collapsed node link positions',
{ tag: ['@canvas', '@node', '@vue-nodes'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('default')
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.canvasOps.resetView()
})
test('link endpoints stay within collapsed node bounds', async ({
comfyPage
}) => {
const node = await comfyPage.vueNodes.getFixtureByTitle(NODE_TITLE)
await node.toggleCollapse()
await comfyPage.nextFrame()
await assertNodeSlotsWithinBounds(comfyPage.page, NODE_ID)
})
test('links follow collapsed node after drag', async ({ comfyPage }) => {
const node = await comfyPage.vueNodes.getFixtureByTitle(NODE_TITLE)
await node.toggleCollapse()
await comfyPage.nextFrame()
await expect.poll(async () => await node.boundingBox()).not.toBeNull()
const box = await node.boundingBox()
await comfyPage.page.mouse.move(
box!.x + box!.width / 2,
box!.y + box!.height / 2
)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(
box!.x + box!.width / 2 + 200,
box!.y + box!.height / 2 + 100,
{ steps: 10 }
)
await comfyPage.page.mouse.up()
await comfyPage.nextFrame()
await assertNodeSlotsWithinBounds(comfyPage.page, NODE_ID)
})
test('links recover correct positions after expand', async ({
comfyPage
}) => {
const node = await comfyPage.vueNodes.getFixtureByTitle(NODE_TITLE)
await node.toggleCollapse()
await comfyPage.nextFrame()
await node.toggleCollapse()
await comfyPage.nextFrame()
await assertNodeSlotsWithinBounds(comfyPage.page, NODE_ID)
})
}
)
test.describe(
'Collapsed node links inside subgraph on first entry',
{ tag: ['@canvas', '@node', '@vue-nodes', '@subgraph', '@screenshot'] },
() => {
test('renders collapsed node links correctly after fitView on first subgraph entry', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-collapsed-node'
)
await comfyPage.nextFrame()
await comfyPage.vueNodes.enterSubgraph('2')
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
// fitView runs on first entry and re-syncs slot layouts for the
// pre-collapsed KSampler. Screenshot captures the rendered canvas
// links to guard against regressing the stale-coordinate bug.
await expect(comfyPage.canvas).toHaveScreenshot(
'subgraph-entry-collapsed-node-links.png'
)
})
}
)