mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-04 03:29:10 +00:00
Compare commits
5 Commits
main
...
fix/subgra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7b48c193c | ||
|
|
38d6a3124e | ||
|
|
1c429d2d57 | ||
|
|
fa0239a9ca | ||
|
|
63a56a8ad1 |
@@ -484,7 +484,16 @@ export class SubgraphHelper {
|
||||
await this.comfyPage.vueNodes.enterSubgraph(hostNodeId)
|
||||
await this.comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
|
||||
await this.comfyPage.nextFrame()
|
||||
await this.comfyPage.canvas.click()
|
||||
await this.comfyPage.canvas.dispatchEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
button: 0
|
||||
})
|
||||
await this.comfyPage.canvas.dispatchEvent('pointerup', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
button: 0
|
||||
})
|
||||
await this.comfyPage.canvas.press('Control+a')
|
||||
await this.comfyPage.nextFrame()
|
||||
await this.page.evaluate(() => {
|
||||
@@ -493,7 +502,16 @@ export class SubgraphHelper {
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
await this.exitViaBreadcrumb()
|
||||
await this.comfyPage.canvas.click()
|
||||
await this.comfyPage.canvas.dispatchEvent('pointerdown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
button: 0
|
||||
})
|
||||
await this.comfyPage.canvas.dispatchEvent('pointerup', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
button: 0
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,16 @@ export class WorkflowHelper {
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
async waitForDraftPersisted({ timeout = 5000 } = {}) {
|
||||
await this.comfyPage.page.waitForFunction(
|
||||
() =>
|
||||
Object.keys(localStorage).some((k) =>
|
||||
k.startsWith('Comfy.Workflow.Draft.v2:')
|
||||
),
|
||||
{ timeout }
|
||||
)
|
||||
}
|
||||
|
||||
async loadWorkflow(workflowName: string) {
|
||||
await this.comfyPage.workflowUploadInput.setInputFiles(
|
||||
assetPath(`${workflowName}.json`)
|
||||
|
||||
78
browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts
Normal file
78
browser_tests/tests/subgraph/subgraphDraftPositions.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe(
|
||||
'Subgraph node positions after draft reload',
|
||||
{ tag: ['@subgraph'] },
|
||||
() => {
|
||||
test('Node positions are preserved after draft reload with subgraph auto-entry', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
test.setTimeout(30000)
|
||||
|
||||
// Enable workflow persistence explicitly
|
||||
await comfyPage.settings.setSetting('Comfy.Workflow.Persist', true)
|
||||
|
||||
// Load a workflow containing a subgraph
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
|
||||
|
||||
// Enter the subgraph programmatically (fixture node is too small for UI click)
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const sg = [...window.app!.rootGraph.subgraphs.values()][0]
|
||||
if (sg) window.app!.canvas.setGraph(sg)
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
|
||||
|
||||
const positionsBefore = await comfyPage.page.evaluate(() => {
|
||||
const sg = [...window.app!.rootGraph.subgraphs.values()][0]
|
||||
return sg.nodes.map((n) => ({
|
||||
id: n.id,
|
||||
x: n.pos[0],
|
||||
y: n.pos[1]
|
||||
}))
|
||||
})
|
||||
|
||||
expect(positionsBefore.length).toBeGreaterThan(0)
|
||||
|
||||
// Wait for the debounced draft persistence to flush to localStorage
|
||||
await comfyPage.workflow.waitForDraftPersisted()
|
||||
|
||||
// Reload the page (draft auto-loads with hash preserved)
|
||||
await comfyPage.page.reload({ waitUntil: 'networkidle' })
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => window.app && window.app.extensionManager
|
||||
)
|
||||
await comfyPage.page.waitForSelector('.p-blockui-mask', {
|
||||
state: 'hidden'
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Wait for subgraph auto-entry via hash navigation
|
||||
await expect
|
||||
.poll(() => comfyPage.subgraph.isInSubgraph(), { timeout: 10000 })
|
||||
.toBe(true)
|
||||
|
||||
// Verify all internal node positions are preserved
|
||||
const positionsAfter = await comfyPage.page.evaluate(() => {
|
||||
const sg = [...window.app!.rootGraph.subgraphs.values()][0]
|
||||
return sg.nodes.map((n) => ({
|
||||
id: n.id,
|
||||
x: n.pos[0],
|
||||
y: n.pos[1]
|
||||
}))
|
||||
})
|
||||
|
||||
for (const before of positionsBefore) {
|
||||
const after = positionsAfter.find((n) => n.id === before.id)
|
||||
expect(
|
||||
after,
|
||||
`Node ${before.id} should exist after reload`
|
||||
).toBeDefined()
|
||||
expect(after!.x).toBeCloseTo(before.x, 0)
|
||||
expect(after!.y).toBeCloseTo(before.y, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -181,7 +181,9 @@ describe('useVueNodeResizeTracking', () => {
|
||||
|
||||
resizeObserverState.callback?.([entry], createObserverMock())
|
||||
|
||||
expect(rectSpy).toHaveBeenCalledTimes(1)
|
||||
// When layout store already has correct position, getBoundingClientRect
|
||||
// is not needed — position is read from the store instead.
|
||||
expect(rectSpy).not.toHaveBeenCalled()
|
||||
expect(testState.setSource).not.toHaveBeenCalled()
|
||||
expect(testState.batchUpdateNodeBounds).not.toHaveBeenCalled()
|
||||
expect(testState.syncNodeSlotLayoutsFromDOM).not.toHaveBeenCalled()
|
||||
@@ -192,13 +194,13 @@ describe('useVueNodeResizeTracking', () => {
|
||||
|
||||
resizeObserverState.callback?.([entry], createObserverMock())
|
||||
|
||||
expect(rectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(rectSpy).not.toHaveBeenCalled()
|
||||
expect(testState.setSource).not.toHaveBeenCalled()
|
||||
expect(testState.batchUpdateNodeBounds).not.toHaveBeenCalled()
|
||||
expect(testState.syncNodeSlotLayoutsFromDOM).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates bounds on first observation when size matches but position differs', () => {
|
||||
it('preserves layout store position when size matches but DOM position differs', () => {
|
||||
const nodeId = 'test-node'
|
||||
const width = 240
|
||||
const height = 180
|
||||
@@ -209,7 +211,6 @@ describe('useVueNodeResizeTracking', () => {
|
||||
left: 100,
|
||||
top: 200
|
||||
})
|
||||
const titleHeight = LiteGraph.NODE_TITLE_HEIGHT
|
||||
|
||||
seedNodeLayout({
|
||||
nodeId,
|
||||
@@ -221,20 +222,10 @@ describe('useVueNodeResizeTracking', () => {
|
||||
|
||||
resizeObserverState.callback?.([entry], createObserverMock())
|
||||
|
||||
expect(rectSpy).toHaveBeenCalledTimes(1)
|
||||
expect(testState.setSource).toHaveBeenCalledWith(LayoutSource.DOM)
|
||||
expect(testState.batchUpdateNodeBounds).toHaveBeenCalledWith([
|
||||
{
|
||||
nodeId,
|
||||
bounds: {
|
||||
x: 100,
|
||||
y: 200 + titleHeight,
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
||||
])
|
||||
expect(testState.syncNodeSlotLayoutsFromDOM).toHaveBeenCalledWith(nodeId)
|
||||
// Position from DOM should NOT override layout store position
|
||||
expect(rectSpy).not.toHaveBeenCalled()
|
||||
expect(testState.setSource).not.toHaveBeenCalled()
|
||||
expect(testState.batchUpdateNodeBounds).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates node bounds + slot layouts when size changes', () => {
|
||||
|
||||
@@ -186,13 +186,26 @@ const resizeObserver = new ResizeObserver((entries) => {
|
||||
continue
|
||||
}
|
||||
|
||||
// Screen-space rect
|
||||
const rect = element.getBoundingClientRect()
|
||||
const [cx, cy] = conv.clientPosToCanvasPos([rect.left, rect.top])
|
||||
const topLeftCanvas = { x: cx, y: cy }
|
||||
// Use existing position from layout store (source of truth) rather than
|
||||
// converting screen-space getBoundingClientRect() back to canvas coords.
|
||||
// The DOM→canvas conversion depends on the current canvas scale/offset,
|
||||
// which can be stale during graph transitions (e.g. entering a subgraph
|
||||
// before fitView runs), producing corrupted positions.
|
||||
const existingPos = nodeLayout?.position
|
||||
let posX: number
|
||||
let posY: number
|
||||
if (existingPos) {
|
||||
posX = existingPos.x
|
||||
posY = existingPos.y
|
||||
} else {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const [cx, cy] = conv.clientPosToCanvasPos([rect.left, rect.top])
|
||||
posX = cx
|
||||
posY = cy + LiteGraph.NODE_TITLE_HEIGHT
|
||||
}
|
||||
const bounds: Bounds = {
|
||||
x: topLeftCanvas.x,
|
||||
y: topLeftCanvas.y + LiteGraph.NODE_TITLE_HEIGHT,
|
||||
x: posX,
|
||||
y: posY,
|
||||
width,
|
||||
height
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user