Compare commits

...

4 Commits

Author SHA1 Message Date
Christian Byrne
0921ec370b Merge branch 'main' into fix/1.42-promoted-widgets-invisible 2026-04-17 20:21:09 -07:00
bymyself
9556b5e333 fix: assert updateWidgets is called on mode switch
Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10999#discussion_r3061095500
2026-04-10 17:50:39 -07:00
bymyself
ec4cdaaede fix: use toggleAppMode() instead of page.evaluate for mode switching
Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10999#discussion_r3061070200
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10999#discussion_r3061079965
2026-04-10 17:50:04 -07:00
bymyself
bfc19dc80d fix: restore promoted widget visibility after graph→app→graph round-trip
Promoted DOM widgets on SubgraphNodes become invisible after switching
from graph mode to app mode and back. The canvas is hidden via v-show
during app mode, so updateWidgets() never runs and widgetState.visible
stays stale (false).

Add a watcher that calls updateWidgets() when leaving linear/app mode
so promoted widgets with positionOverride are correctly marked visible.
2026-04-09 14:39:16 -07:00
3 changed files with 108 additions and 0 deletions

View File

@@ -159,5 +159,35 @@ test.describe('Subgraph Promotion DOM', { tag: ['@subgraph'] }, () => {
await expect(visibleWidgets).toHaveCount(parentCount)
})
test('Promoted DOM widget is visible after graph → app → graph round-trip', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)
await comfyPage.nextFrame()
const domWidgets = comfyPage.page.locator(DOM_WIDGET_SELECTOR)
await expect(domWidgets).toBeVisible()
await expect(domWidgets).toHaveCount(1)
// Switch to app mode (linear mode)
await comfyPage.appMode.toggleAppMode()
const graphContainer = comfyPage.page.locator('#graph-canvas-container')
await expect(graphContainer).toBeHidden()
// Switch back to graph mode
await comfyPage.appMode.toggleAppMode()
await comfyPage.nextFrame()
await expect(graphContainer).toBeVisible()
// Without the fix, widgetState.visible stays stale (false) because
// updateWidgets() never ran while the canvas was hidden via v-show.
await expect(domWidgets).toBeVisible({ timeout: 3000 })
await expect(domWidgets).toHaveCount(1)
})
})
})

View File

@@ -2,12 +2,17 @@ import { createTestingPinia } from '@pinia/testing'
import { fromPartial } from '@total-typescript/shoehorn'
import { render } from '@testing-library/vue'
import { setActivePinia } from 'pinia'
import { nextTick } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import DomWidgets from '@/components/graph/DomWidgets.vue'
import { useAppMode } from '@/composables/useAppMode'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type { LoadedComfyWorkflow } from '@/platform/workflow/management/stores/comfyWorkflow'
import { ComfyWorkflow } from '@/platform/workflow/management/stores/comfyWorkflow'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { BaseDOMWidget } from '@/scripts/domWidget'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
@@ -57,6 +62,73 @@ function drawFrame(canvas: LGraphCanvas) {
canvas.onDrawForeground?.({} as CanvasRenderingContext2D, new Rectangle())
}
describe('DomWidgets app mode round-trip', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('restores promoted widget visibility after graph → app → graph', async () => {
const canvasStore = useCanvasStore()
const domWidgetStore = useDomWidgetStore()
const { setMode } = useAppMode()
// Set up an active workflow so linearMode is driven by activeMode
const workflowStore = useWorkflowStore()
const workflow = new ComfyWorkflow({
path: 'workflows/test.json',
modified: Date.now(),
size: 1
})
workflow.activeMode = 'graph'
workflowStore.activeWorkflow = workflow as unknown as LoadedComfyWorkflow
const rootGraph = new LGraph()
const subgraphGraph = new LGraph()
const interiorNode = createNode(subgraphGraph, 1, 'interior', [50, 50])
const subgraphNode = createNode(rootGraph, 2, 'subgraph', [300, 300])
const widget = createWidget('promoted-widget', interiorNode, 10)
const overrideWidget = createWidget('override-src', subgraphNode, 20)
domWidgetStore.registerWidget(widget)
domWidgetStore.setPositionOverride(widget.id, {
node: subgraphNode,
widget: overrideWidget
})
// Interior widget is inactive (its node lives in the subgraph, not root)
domWidgetStore.deactivateWidget(widget.id)
const canvas = createCanvas(rootGraph)
canvasStore.canvas = canvas
render(DomWidgets, {
global: { stubs: { DomWidget: true } }
})
// Initial draw — promoted widget should be visible via positionOverride
drawFrame(canvas)
const widgetState = domWidgetStore.widgetStates.get(widget.id)!
expect(widgetState.visible).toBe(true)
// Enter app mode — canvas is hidden, no more draw calls
setMode('app')
await nextTick()
// Simulate stale visibility from lack of draw calls during app mode
// (in production, v-show hides the canvas so updateWidgets doesn't run)
widgetState.visible = false
vi.mocked(canvas.isNodeVisible).mockClear()
// Return to graph mode
setMode('graph')
await nextTick()
// The whenever watcher should have called updateWidgets automatically
expect(canvas.isNodeVisible).toHaveBeenCalled()
expect(widgetState.visible).toBe(true)
})
})
describe('DomWidgets transition grace characterization', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))

View File

@@ -113,4 +113,10 @@ whenever(
)),
{ immediate: true }
)
// When returning from app mode, the canvas was hidden (v-show) so
// updateWidgets() hasn't run — widgetState.visible is stale.
// Run it immediately so promoted widgets with positionOverride are
// correctly marked visible before DomWidget tries to mount elements.
whenever(() => !canvasStore.linearMode, updateWidgets)
</script>