mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary Fix connection links rendering at wrong positions when nodes are collapsed in Vue nodes mode. ## Changes - **What**: Fall back to `clientPosToCanvasPos` for collapsed node slot positioning since DOM-relative scale derivation is invalid when layout store preserves expanded size. Clear stale `cachedOffset` on collapse and defer sync when canvas is not yet initialized. - 3 unit tests for collapsed node slot sync fallback (clientPosToCanvasPos, cachedOffset clearing, canvas-not-initialized deferral) - 3 E2E tests for collapsed node link positions (within bounds, after position change, after expand recovery) ## Review Focus - `clientPosToCanvasPos` fallback is safe for collapsed nodes because collapse is user-initiated (no loading-time transform desync risk that #9121 originally fixed) - `cachedOffset` clearing prevents stale expanded-state offsets during collapsed node drag - Regression from #9121 (DOM-relative scale) combined with #9680 (collapsed node ResizeObserver skip) ## Screenshots Before <img width="1030" height="434" alt="image" src="https://github.com/user-attachments/assets/2f8b8a1f-ed22-4588-ab62-72b89880e53f" /> After <img width="1029" height="476" alt="image" src="https://github.com/user-attachments/assets/52dbbf7c-61ed-465b-ae19-a9781513e7e8" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10641-fix-collapsed-node-connection-link-positions-3316d73d365081f4aee3fecb92c83b91) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
99 lines
2.6 KiB
TypeScript
99 lines
2.6 KiB
TypeScript
import { expect } from '@playwright/test'
|
|
import type { Page } from '@playwright/test'
|
|
|
|
export interface SlotMeasurement {
|
|
key: string
|
|
offsetX: number
|
|
offsetY: number
|
|
}
|
|
|
|
export interface NodeSlotData {
|
|
nodeId: string
|
|
nodeW: number
|
|
nodeH: number
|
|
slots: SlotMeasurement[]
|
|
}
|
|
|
|
/**
|
|
* Collect slot center offsets relative to the parent node element.
|
|
* Returns `null` when the node element is not found.
|
|
*/
|
|
export async function measureNodeSlotOffsets(
|
|
page: Page,
|
|
nodeId: string
|
|
): Promise<NodeSlotData | null> {
|
|
return page.evaluate((id) => {
|
|
const nodeEl = document.querySelector(`[data-node-id="${id}"]`)
|
|
if (!nodeEl || !(nodeEl instanceof HTMLElement)) return null
|
|
|
|
const nodeRect = nodeEl.getBoundingClientRect()
|
|
const slotEls = nodeEl.querySelectorAll('[data-slot-key]')
|
|
const slots: SlotMeasurement[] = []
|
|
|
|
for (const slotEl of slotEls) {
|
|
const slotRect = slotEl.getBoundingClientRect()
|
|
slots.push({
|
|
key: (slotEl as HTMLElement).dataset.slotKey ?? 'unknown',
|
|
offsetX: slotRect.left + slotRect.width / 2 - nodeRect.left,
|
|
offsetY: slotRect.top + slotRect.height / 2 - nodeRect.top
|
|
})
|
|
}
|
|
|
|
return {
|
|
nodeId: id,
|
|
nodeW: nodeRect.width,
|
|
nodeH: nodeRect.height,
|
|
slots
|
|
}
|
|
}, nodeId)
|
|
}
|
|
|
|
/**
|
|
* Assert that every slot falls within the node dimensions (± `margin` px).
|
|
*/
|
|
export function expectSlotsWithinBounds(
|
|
data: NodeSlotData,
|
|
margin: number,
|
|
label?: string
|
|
) {
|
|
const prefix = label ? `${label}: ` : ''
|
|
|
|
for (const slot of data.slots) {
|
|
expect(
|
|
slot.offsetX,
|
|
`${prefix}Slot ${slot.key} X=${slot.offsetX} outside width=${data.nodeW}`
|
|
).toBeGreaterThanOrEqual(-margin)
|
|
expect(
|
|
slot.offsetX,
|
|
`${prefix}Slot ${slot.key} X=${slot.offsetX} outside width=${data.nodeW}`
|
|
).toBeLessThanOrEqual(data.nodeW + margin)
|
|
|
|
expect(
|
|
slot.offsetY,
|
|
`${prefix}Slot ${slot.key} Y=${slot.offsetY} outside height=${data.nodeH}`
|
|
).toBeGreaterThanOrEqual(-margin)
|
|
expect(
|
|
slot.offsetY,
|
|
`${prefix}Slot ${slot.key} Y=${slot.offsetY} outside height=${data.nodeH}`
|
|
).toBeLessThanOrEqual(data.nodeH + margin)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for slots, measure, and assert within bounds — single-node convenience.
|
|
*/
|
|
export async function assertNodeSlotsWithinBounds(
|
|
page: Page,
|
|
nodeId: string,
|
|
margin: number = 20
|
|
) {
|
|
await page
|
|
.locator(`[data-node-id="${nodeId}"] [data-slot-key]`)
|
|
.first()
|
|
.waitFor()
|
|
|
|
const data = await measureNodeSlotOffsets(page, nodeId)
|
|
expect(data, `Node ${nodeId} not found in DOM`).not.toBeNull()
|
|
expectSlotsWithinBounds(data!, margin, `Node ${nodeId}`)
|
|
}
|