Compare commits

...

5 Commits

Author SHA1 Message Date
bymyself
e87c4866f5 fix: dirty root canvas for subgraph LoadImage preview + add debug logging 2026-03-26 21:47:28 -07:00
Alexander Brown
ad459aae94 Merge branch 'main' into fix/loadimage-subgraph-preview 2026-03-26 11:53:54 -07:00
bymyself
1dc8d41515 fix: stabilize LoadImage subgraph E2E test for CI
Load default workflow and pan to node before context menu
interaction, matching the pattern used by other subgraph tests.
2026-03-25 14:25:30 -07:00
bymyself
da74bd3997 test: add Playwright test for LoadImage subgraph preview promotion
Verifies that converting a LoadImage node to a subgraph produces the
$$canvas-image-preview pseudo-widget in the subgraph node's
proxyWidgets, confirming the fix in canvasImagePreviewTypes.ts.
2026-03-25 13:55:12 -07:00
bymyself
65fb3483fc fix: add LoadImage and LoadVideo to canvas preview node types
LoadImage and LoadVideo were missing from CANVAS_IMAGE_PREVIEW_NODE_TYPES,
so their previews were never promoted to parent subgraph nodes. This caused
promoted preview images to not display on subgraph nodes in graph mode and
app mode when the interior node was a LoadImage or LoadVideo.
2026-03-25 13:39:45 -07:00
5 changed files with 103 additions and 2 deletions

View File

@@ -8,7 +8,8 @@ import { fitToViewInstant } from '../helpers/fitToView'
import {
getPromotedWidgetNames,
getPromotedWidgetCount,
getPromotedWidgets
getPromotedWidgets,
getPseudoPreviewWidgets
} from '../helpers/promotedWidgets'
test.describe(
@@ -93,6 +94,34 @@ test.describe(
// SaveImage is in the recommendedNodes list, so filename_prefix is promoted
expect(promotedNames).toContain('filename_prefix')
})
test('LoadImage node gets $$canvas-image-preview pseudo-widget promoted', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('default')
// Add a LoadImage node and convert to subgraph programmatically
const subgraphNodeId = await comfyPage.page.evaluate(() => {
const graph = window.app!.graph!
const node = window.LiteGraph!.createNode('LoadImage')!
node.pos = [300, 300]
graph.add(node)
const { node: subgraphNode } = graph.convertToSubgraph(
new Set([node])
)
return String(subgraphNode.id)
})
await comfyPage.nextFrame()
const pseudoWidgets = await getPseudoPreviewWidgets(
comfyPage,
subgraphNodeId
)
expect(pseudoWidgets.length).toBeGreaterThan(0)
expect(
pseudoWidgets.some(([, name]) => name === '$$canvas-image-preview')
).toBe(true)
})
})
test.describe('Promoted Widget Visibility in LiteGraph Mode', () => {

View File

@@ -4,7 +4,9 @@ export const CANVAS_IMAGE_PREVIEW_WIDGET = '$$canvas-image-preview'
const CANVAS_IMAGE_PREVIEW_NODE_TYPES = new Set([
'PreviewImage',
'SaveImage',
'GLSLShader'
'GLSLShader',
'LoadImage',
'LoadVideo'
])
export function supportsVirtualCanvasImagePreview(node: LGraphNode): boolean {

View File

@@ -51,10 +51,22 @@ export function usePromotedPreviews(
)
const reactiveOutputs = nodeOutputStore.nodeOutputs[locatorId]
const reactivePreviews = nodeOutputStore.nodePreviewImages[locatorId]
console.warn('[PROMOTED-PREVIEW]', {
locatorId,
hasOutputs: !!reactiveOutputs?.images?.length,
hasPreviews: !!reactivePreviews?.length,
entry
})
if (!reactiveOutputs?.images?.length && !reactivePreviews?.length)
continue
const urls = nodeOutputStore.getNodeImageUrls(interiorNode)
console.warn(
'[PROMOTED-PREVIEW] urls:',
urls?.length,
'type:',
interiorNode.previewMediaType
)
if (!urls?.length) continue
const type =

View File

@@ -201,6 +201,28 @@ describe('getPromotableWidgets', () => {
).toBe(true)
})
it('adds virtual canvas preview widget for LoadImage nodes', () => {
const node = new LGraphNode('LoadImage')
node.type = 'LoadImage'
const widgets = getPromotableWidgets(node)
expect(
widgets.some((widget) => widget.name === CANVAS_IMAGE_PREVIEW_WIDGET)
).toBe(true)
})
it('adds virtual canvas preview widget for LoadVideo nodes', () => {
const node = new LGraphNode('LoadVideo')
node.type = 'LoadVideo'
const widgets = getPromotableWidgets(node)
expect(
widgets.some((widget) => widget.name === CANVAS_IMAGE_PREVIEW_WIDGET)
).toBe(true)
})
it('does not add virtual canvas preview widget for non-image nodes', () => {
const node = new LGraphNode('TextNode')
node.addOutput('TEXT', 'STRING')
@@ -269,6 +291,25 @@ describe('promoteRecommendedWidgets', () => {
expect(updatePreviewsMock).not.toHaveBeenCalled()
})
it('eagerly promotes virtual preview widget for LoadImage nodes', () => {
const subgraph = createTestSubgraph()
const subgraphNode = createTestSubgraphNode(subgraph)
const loadImageNode = new LGraphNode('LoadImage')
loadImageNode.type = 'LoadImage'
subgraph.add(loadImageNode)
promoteRecommendedWidgets(subgraphNode)
const store = usePromotionStore()
expect(
store.isPromoted(subgraphNode.rootGraph.id, subgraphNode.id, {
sourceNodeId: String(loadImageNode.id),
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET
})
).toBe(true)
expect(updatePreviewsMock).not.toHaveBeenCalled()
})
it('registers $$canvas-image-preview on configure for GLSLShader in saved workflow', () => {
// Simulate loading a saved workflow where proxyWidgets does NOT contain
// the $$canvas-image-preview entry (e.g. blueprint authored before the

View File

@@ -103,11 +103,28 @@ export const useImageUploadWidget = () => {
// Add our own callback to the combo widget to render an image when it changes
fileComboWidget.callback = function () {
console.warn(
'[IMAGEUPLOAD-CB] value:',
fileComboWidget.value,
'graphId:',
node.graph?.id,
'nodeId:',
node.id,
'inSubgraph:',
node.graph !== node.graph?.rootGraph
)
node.imgs = undefined
nodeOutputStore.setNodeOutputs(node, String(fileComboWidget.value), {
isAnimated
})
node.graph?.setDirtyCanvas(true)
// When this node is inside a subgraph, also dirty the root canvas so
// the parent SubgraphNode redraws and picks up the new preview via
// its onDrawBackground → updatePreviews loop.
const rootGraph = node.graph?.rootGraph
if (rootGraph && rootGraph !== node.graph) {
rootGraph.setDirtyCanvas(true)
}
}
// On load if we have a value then render the image