mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-05 12:44:23 +00:00
Compare commits
5 Commits
version-bu
...
glary/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
694d96d080 | ||
|
|
77066044cb | ||
|
|
68be64c6dd | ||
|
|
b81674015c | ||
|
|
9c88bad70b |
168
browser_tests/tests/subgraph/subgraphCollapseDomWidgets.spec.ts
Normal file
168
browser_tests/tests/subgraph/subgraphCollapseDomWidgets.spec.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
const DOM_WIDGET_SELECTOR = '.comfy-multiline-input'
|
||||
const VISIBLE_DOM_WIDGET_SELECTOR = `${DOM_WIDGET_SELECTOR}:visible`
|
||||
|
||||
async function toggleSubgraphCollapse(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string
|
||||
): Promise<void> {
|
||||
await comfyPage.page.evaluate((id) => {
|
||||
const node = window.app!.graph!.getNodeById(id)
|
||||
if (!node) throw new Error(`Node ${id} not found`)
|
||||
node.collapse()
|
||||
}, nodeId)
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
async function setVueMode(
|
||||
comfyPage: ComfyPage,
|
||||
enabled: boolean
|
||||
): Promise<void> {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', enabled)
|
||||
if (enabled) {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
}
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
test.describe(
|
||||
'Subgraph collapse hides DOM widgets',
|
||||
{ tag: ['@subgraph'] },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
|
||||
})
|
||||
|
||||
test('promoted DOM widget is hidden when SubgraphNode collapses and restored on expand', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
const visibleWidgets = comfyPage.page.locator(VISIBLE_DOM_WIDGET_SELECTOR)
|
||||
await expect(
|
||||
visibleWidgets,
|
||||
'Promoted text widget should be visible before collapse'
|
||||
).toHaveCount(1)
|
||||
|
||||
await toggleSubgraphCollapse(comfyPage, '11')
|
||||
|
||||
await expect(visibleWidgets).toHaveCount(0)
|
||||
|
||||
await toggleSubgraphCollapse(comfyPage, '11')
|
||||
|
||||
await expect(visibleWidgets).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('all promoted DOM widgets are hidden when SubgraphNode collapses', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-multiple-promoted-widgets'
|
||||
)
|
||||
|
||||
const visibleWidgets = comfyPage.page.locator(VISIBLE_DOM_WIDGET_SELECTOR)
|
||||
await expect(
|
||||
visibleWidgets,
|
||||
'Both promoted text widgets should be visible before collapse'
|
||||
).toHaveCount(2)
|
||||
|
||||
await toggleSubgraphCollapse(comfyPage, '11')
|
||||
|
||||
await expect(visibleWidgets).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('DOM widgets stay hidden after entering and exiting a collapsed subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
await toggleSubgraphCollapse(comfyPage, '11')
|
||||
await expect(
|
||||
comfyPage.page.locator(VISIBLE_DOM_WIDGET_SELECTOR)
|
||||
).toHaveCount(0)
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.graph!.nodes.find(
|
||||
(n) => typeof n.isSubgraphNode === 'function' && n.isSubgraphNode()
|
||||
)
|
||||
if (!node || !('subgraph' in node))
|
||||
throw new Error('SubgraphNode not found')
|
||||
window.app!.canvas.openSubgraph(node.subgraph, node)
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator(VISIBLE_DOM_WIDGET_SELECTOR)
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('no ghost DOM widgets on collapsed subgraph after Vue-to-Legacy toggle', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
const visibleWidgets = comfyPage.page.locator(VISIBLE_DOM_WIDGET_SELECTOR)
|
||||
await expect(visibleWidgets).toHaveCount(1)
|
||||
|
||||
await toggleSubgraphCollapse(comfyPage, '11')
|
||||
await expect(visibleWidgets).toHaveCount(0)
|
||||
|
||||
await setVueMode(comfyPage, true)
|
||||
await setVueMode(comfyPage, false)
|
||||
|
||||
await expect(visibleWidgets).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('no ghost DOM widgets after repeated renderer toggles on collapsed subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
await toggleSubgraphCollapse(comfyPage, '11')
|
||||
const visibleWidgets = comfyPage.page.locator(VISIBLE_DOM_WIDGET_SELECTOR)
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await setVueMode(comfyPage, true)
|
||||
await setVueMode(comfyPage, false)
|
||||
}
|
||||
|
||||
await expect(visibleWidgets).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('widgets reappear after mode toggle then expand on collapsed subgraph', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-promoted-text-widget'
|
||||
)
|
||||
|
||||
await toggleSubgraphCollapse(comfyPage, '11')
|
||||
|
||||
await setVueMode(comfyPage, true)
|
||||
await setVueMode(comfyPage, false)
|
||||
|
||||
await toggleSubgraphCollapse(comfyPage, '11')
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator(VISIBLE_DOM_WIDGET_SELECTOR)
|
||||
).toHaveCount(1)
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -57,6 +57,77 @@ function drawFrame(canvas: LGraphCanvas) {
|
||||
canvas.onDrawForeground?.({} as CanvasRenderingContext2D, new Rectangle())
|
||||
}
|
||||
|
||||
describe('DomWidgets collapsed node visibility', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('hides widget when position-override node is collapsed', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const graph = new LGraph()
|
||||
const interiorNode = createNode(graph, 1, 'interior', [100, 200])
|
||||
const overrideNode = createNode(graph, 2, 'subgraphHost', [300, 400])
|
||||
|
||||
const widget = createWidget('w-collapse-override', interiorNode, 14)
|
||||
const overrideWidget = createWidget('w-override-pos', overrideNode, 22)
|
||||
|
||||
domWidgetStore.registerWidget(widget)
|
||||
domWidgetStore.setPositionOverride(widget.id, {
|
||||
node: overrideNode,
|
||||
widget: overrideWidget
|
||||
})
|
||||
|
||||
const canvas = createCanvas(graph)
|
||||
canvasStore.canvas = canvas
|
||||
render(DomWidgets, { global: { stubs: { DomWidget: true } } })
|
||||
|
||||
// Expanded — widget visible
|
||||
drawFrame(canvas)
|
||||
const state = domWidgetStore.widgetStates.get(widget.id)!
|
||||
expect(state.visible).toBe(true)
|
||||
|
||||
// Collapse override node — widget hidden
|
||||
overrideNode.flags.collapsed = true
|
||||
drawFrame(canvas)
|
||||
expect(state.visible).toBe(false)
|
||||
|
||||
// Expand — widget restored
|
||||
overrideNode.flags.collapsed = false
|
||||
drawFrame(canvas)
|
||||
expect(state.visible).toBe(true)
|
||||
})
|
||||
|
||||
it('hides widget when own node (no override) is collapsed', () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
|
||||
const graph = new LGraph()
|
||||
const node = createNode(graph, 1, 'collapsible', [100, 200])
|
||||
|
||||
const widget = createWidget('w-collapse-own', node, 14)
|
||||
|
||||
domWidgetStore.registerWidget(widget)
|
||||
|
||||
const canvas = createCanvas(graph)
|
||||
canvasStore.canvas = canvas
|
||||
render(DomWidgets, { global: { stubs: { DomWidget: true } } })
|
||||
|
||||
drawFrame(canvas)
|
||||
const state = domWidgetStore.widgetStates.get(widget.id)!
|
||||
expect(state.visible).toBe(true)
|
||||
|
||||
node.flags.collapsed = true
|
||||
drawFrame(canvas)
|
||||
expect(state.visible).toBe(false)
|
||||
|
||||
node.flags.collapsed = false
|
||||
drawFrame(canvas)
|
||||
expect(state.visible).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DomWidgets transition grace characterization', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
|
||||
@@ -75,10 +75,11 @@ const updateWidgets = () => {
|
||||
|
||||
const isInCorrectGraph = posNode.graph === currentGraph
|
||||
const nodeVisible = lgCanvas.isNodeVisible(posNode)
|
||||
const shouldBeVisible = nodeVisible && !posNode.collapsed
|
||||
|
||||
widgetState.visible =
|
||||
shouldBeVisible &&
|
||||
isInCorrectGraph &&
|
||||
nodeVisible &&
|
||||
!(widget.options.hideOnZoom && lowQuality)
|
||||
|
||||
if (widgetState.visible) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
|
||||
function useVueNodeLifecycleIndividual() {
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -87,6 +88,16 @@ function useVueNodeLifecycleIndividual() {
|
||||
() => {
|
||||
disposeNodeManagerAndSyncs()
|
||||
|
||||
// Clear stale DOM widget position overrides from the Vue rendering
|
||||
// cycle. The legacy renderer re-establishes needed overrides during
|
||||
// its first draw pass via PromotedWidgetView.draw / y setter.
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
for (const [widgetId, state] of domWidgetStore.widgetStates) {
|
||||
if (state.positionOverride) {
|
||||
domWidgetStore.clearPositionOverride(widgetId)
|
||||
}
|
||||
}
|
||||
|
||||
// Force arrange() on all nodes so input.pos is computed before
|
||||
// the first legacy drawConnections frame (which may run before
|
||||
// drawNode on the foreground canvas).
|
||||
|
||||
@@ -2656,6 +2656,51 @@ describe('DOM widget promotion', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('syncDomOverride skips override when subgraphNode is collapsed', () => {
|
||||
const [subgraphNode, innerNodes, innerIds] = setupSubgraph(1)
|
||||
createMockDOMWidget(innerNodes[0], 'textarea')
|
||||
setPromotions(subgraphNode, [[innerIds[0], 'textarea']])
|
||||
|
||||
subgraphNode.flags.collapsed = true
|
||||
mockDomWidgetStore.setPositionOverride.mockClear()
|
||||
|
||||
const view = subgraphNode.widgets[0]
|
||||
view.draw!(createFakeCanvasContext(), subgraphNode, 200, 0, 30)
|
||||
|
||||
expect(mockDomWidgetStore.setPositionOverride).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('syncDomOverride sets override when subgraphNode is expanded', () => {
|
||||
const [subgraphNode, innerNodes, innerIds] = setupSubgraph(1)
|
||||
createMockDOMWidget(innerNodes[0], 'textarea')
|
||||
setPromotions(subgraphNode, [[innerIds[0], 'textarea']])
|
||||
|
||||
subgraphNode.flags.collapsed = false
|
||||
mockDomWidgetStore.setPositionOverride.mockClear()
|
||||
|
||||
const view = subgraphNode.widgets[0]
|
||||
view.draw!(createFakeCanvasContext(), subgraphNode, 200, 0, 30)
|
||||
|
||||
expect(mockDomWidgetStore.setPositionOverride).toHaveBeenCalledWith(
|
||||
'dom-widget-textarea',
|
||||
{ node: subgraphNode, widget: view }
|
||||
)
|
||||
})
|
||||
|
||||
test('y setter does not trigger override when subgraphNode is collapsed', () => {
|
||||
const [subgraphNode, innerNodes, innerIds] = setupSubgraph(1)
|
||||
createMockDOMWidget(innerNodes[0], 'textarea')
|
||||
setPromotions(subgraphNode, [[innerIds[0], 'textarea']])
|
||||
|
||||
subgraphNode.flags.collapsed = true
|
||||
mockDomWidgetStore.setPositionOverride.mockClear()
|
||||
|
||||
const view = subgraphNode.widgets[0]
|
||||
view.y = 100
|
||||
|
||||
expect(mockDomWidgetStore.setPositionOverride).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('onRemoved clears position overrides for all promoted DOM widgets', () => {
|
||||
const [subgraphNode, innerNodes, innerIds] = setupSubgraph(1)
|
||||
createMockDOMWidget(innerNodes[0], 'widgetA')
|
||||
|
||||
@@ -535,6 +535,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
| undefined = this.resolveAtHost()
|
||||
) {
|
||||
if (!resolved || !isBaseDOMWidget(resolved.widget)) return
|
||||
if (this.subgraphNode.flags?.collapsed) return
|
||||
useDomWidgetStore().setPositionOverride(resolved.widget.id, {
|
||||
node: this.subgraphNode,
|
||||
widget: this
|
||||
|
||||
Reference in New Issue
Block a user