mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: ensure promotionStore mutations trigger reactive re-evaluation
Add version counter to promotionStore that guarantees computed properties re-evaluate when promotions change. The nested Map<UUID, Map<NodeId, ...>> structure can miss reactive notifications when inner Maps are lazily initialized and returned as raw references. - Add _version ref incremented on setPromotions/movePromotion/clearGraph - Read _version in getPromotionsRef to establish reactive dependency - Re-read inner Maps through reactive proxy after lazy init - Add unit test for late-promotion reactivity scenario - Add E2E test for promoted preview rendering via synthetic execution
This commit is contained in:
@@ -553,6 +553,48 @@ test.describe(
|
||||
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-5"]')
|
||||
await expect(nodeBody).toBeVisible()
|
||||
})
|
||||
|
||||
test('Promoted preview renders when outputs are injected for interior node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-with-preview-node'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const subgraphVueNode = comfyPage.vueNodes.getNodeLocator('5')
|
||||
await expect(subgraphVueNode).toBeVisible()
|
||||
|
||||
// Simulate backend sending execution results for interior SaveImage
|
||||
// node (id 10) inside the SubgraphNode (id 5).
|
||||
// The executed event handler converts execution ID "5:10" to a
|
||||
// NodeLocatorId using the subgraph UUID, then sets both
|
||||
// app.nodeOutputs and the reactive nodeOutputs ref.
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window.app!.api
|
||||
api.dispatchEvent(
|
||||
new CustomEvent('executed', {
|
||||
detail: {
|
||||
node: '5:10',
|
||||
display_node: '5:10',
|
||||
output: {
|
||||
images: [
|
||||
{
|
||||
filename: 'example.png',
|
||||
subfolder: '',
|
||||
type: 'input'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// The promoted preview should render an image inside the SubgraphNode
|
||||
const previewImage = subgraphVueNode.locator('.image-preview img')
|
||||
await expect(previewImage).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Nested Promoted Widget Disabled State', () => {
|
||||
|
||||
@@ -266,6 +266,38 @@ describe(usePromotedPreviews, () => {
|
||||
expect(promotedPreviews.value).toEqual([])
|
||||
})
|
||||
|
||||
it('re-evaluates when promotion is added after initial mount', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10, previewMediaType: 'image' })
|
||||
|
||||
// Mount BEFORE any promotions exist
|
||||
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
|
||||
expect(promotedPreviews.value).toEqual([])
|
||||
|
||||
// Add promotion AFTER mount
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
'10',
|
||||
'$$canvas-image-preview'
|
||||
)
|
||||
|
||||
// Seed outputs
|
||||
const mockUrls = ['/view?filename=output.png']
|
||||
seedOutputs(setup.subgraph.id, [10])
|
||||
getNodeImageUrls.mockReturnValue(mockUrls)
|
||||
|
||||
// The computed should re-evaluate and return the preview
|
||||
expect(promotedPreviews.value).toEqual([
|
||||
{
|
||||
interiorNodeId: '10',
|
||||
widgetName: '$$canvas-image-preview',
|
||||
type: 'image',
|
||||
urls: mockUrls
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores non-$$ promoted widgets', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10 })
|
||||
|
||||
@@ -13,24 +13,35 @@ export const usePromotionStore = defineStore('promotion', () => {
|
||||
const graphPromotions = ref(new Map<UUID, Map<NodeId, PromotionEntry[]>>())
|
||||
const graphRefCounts = ref(new Map<UUID, Map<string, number>>())
|
||||
|
||||
/**
|
||||
* Monotonic counter incremented on every mutation. Dependents read this
|
||||
* inside their computed/watch to guarantee re-evaluation even when the
|
||||
* nested Map proxies don't propagate change notifications reliably.
|
||||
*/
|
||||
const _version = ref(0)
|
||||
function _touch() {
|
||||
_version.value++
|
||||
}
|
||||
|
||||
function _getPromotionsForGraph(
|
||||
graphId: UUID
|
||||
): Map<NodeId, PromotionEntry[]> {
|
||||
const promotions = graphPromotions.value.get(graphId)
|
||||
let promotions = graphPromotions.value.get(graphId)
|
||||
if (promotions) return promotions
|
||||
|
||||
const nextPromotions = new Map<NodeId, PromotionEntry[]>()
|
||||
graphPromotions.value.set(graphId, nextPromotions)
|
||||
return nextPromotions
|
||||
graphPromotions.value.set(graphId, new Map<NodeId, PromotionEntry[]>())
|
||||
// Re-read through the reactive proxy so callers get the tracked version
|
||||
promotions = graphPromotions.value.get(graphId)!
|
||||
return promotions
|
||||
}
|
||||
|
||||
function _getRefCountsForGraph(graphId: UUID): Map<string, number> {
|
||||
const refCounts = graphRefCounts.value.get(graphId)
|
||||
let refCounts = graphRefCounts.value.get(graphId)
|
||||
if (refCounts) return refCounts
|
||||
|
||||
const nextRefCounts = new Map<string, number>()
|
||||
graphRefCounts.value.set(graphId, nextRefCounts)
|
||||
return nextRefCounts
|
||||
graphRefCounts.value.set(graphId, new Map<string, number>())
|
||||
refCounts = graphRefCounts.value.get(graphId)!
|
||||
return refCounts
|
||||
}
|
||||
|
||||
function _makeKey(interiorNodeId: string, widgetName: string): string {
|
||||
@@ -62,6 +73,8 @@ export const usePromotionStore = defineStore('promotion', () => {
|
||||
graphId: UUID,
|
||||
subgraphNodeId: NodeId
|
||||
): PromotionEntry[] {
|
||||
// Read version to establish reactive dependency
|
||||
void _version.value
|
||||
return _getPromotionsForGraph(graphId).get(subgraphNodeId) ?? []
|
||||
}
|
||||
|
||||
@@ -107,6 +120,7 @@ export const usePromotionStore = defineStore('promotion', () => {
|
||||
} else {
|
||||
promotions.set(subgraphNodeId, [...entries])
|
||||
}
|
||||
_touch()
|
||||
}
|
||||
|
||||
function promote(
|
||||
@@ -165,11 +179,13 @@ export const usePromotionStore = defineStore('promotion', () => {
|
||||
|
||||
// Reordering does not change membership, so ref-counts remain valid.
|
||||
promotions.set(subgraphNodeId, entries)
|
||||
_touch()
|
||||
}
|
||||
|
||||
function clearGraph(graphId: UUID): void {
|
||||
graphPromotions.value.delete(graphId)
|
||||
graphRefCounts.value.delete(graphId)
|
||||
_touch()
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user