diff --git a/browser_tests/tests/subgraphProgressClear.spec.ts b/browser_tests/tests/subgraphProgressClear.spec.ts new file mode 100644 index 0000000000..8e54a90c22 --- /dev/null +++ b/browser_tests/tests/subgraphProgressClear.spec.ts @@ -0,0 +1,77 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.describe( + 'Subgraph progress clear on navigation', + { tag: ['@subgraph'] }, + () => { + test('Stale progress is cleared on subgraph node after navigating back', async ({ + comfyPage + }) => { + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.nextFrame() + + // Find the subgraph node + const subgraphNodeId = await comfyPage.page.evaluate(() => { + const graph = window.app!.canvas.graph! + const subgraphNode = graph.nodes.find( + (n) => typeof n.isSubgraphNode === 'function' && n.isSubgraphNode() + ) + return subgraphNode ? String(subgraphNode.id) : null + }) + expect(subgraphNodeId).not.toBeNull() + + // Simulate a stale progress value on the subgraph node. + // This happens when: + // 1. User views root graph during execution + // 2. Progress watcher sets node.progress = 0.5 + // 3. User enters subgraph + // 4. Execution completes (nodeProgressStates becomes {}) + // 5. Watcher fires, clears subgraph-internal nodes, but root-level + // SubgraphNode isn't visible so it keeps stale progress + // 6. User navigates back — watcher should fire and clear it + await comfyPage.page.evaluate((nodeId) => { + const node = window.app!.canvas.graph!.getNodeById(nodeId)! + node.progress = 0.5 + }, subgraphNodeId!) + + // Verify progress is set + const progressBefore = await comfyPage.page.evaluate((nodeId) => { + return window.app!.canvas.graph!.getNodeById(nodeId)!.progress + }, subgraphNodeId!) + expect(progressBefore).toBe(0.5) + + // Navigate into the subgraph + const subgraphNode = await comfyPage.nodeOps.getNodeRefById( + subgraphNodeId! + ) + await subgraphNode.navigateIntoSubgraph() + + // Verify we're inside the subgraph + const inSubgraph = await comfyPage.page.evaluate(() => { + const graph = window.app!.canvas.graph + return !!graph && 'inputNode' in graph + }) + expect(inSubgraph).toBe(true) + + // Navigate back to the root graph + await comfyPage.page.keyboard.press('Escape') + await comfyPage.nextFrame() + + // The progress watcher should fire when graph changes (because + // nodeLocationProgressStates is empty {} and the watcher should + // iterate canvas.graph.nodes to clear stale node.progress values). + // + // BUG: Without watching canvasStore.currentGraph, the watcher doesn't + // fire on subgraph->root navigation when progress is already empty, + // leaving stale node.progress = 0.5 on the SubgraphNode. + await expect(async () => { + const progressAfter = await comfyPage.page.evaluate((nodeId) => { + return window.app!.canvas.graph!.getNodeById(nodeId)!.progress + }, subgraphNodeId!) + expect(progressAfter).toBeUndefined() + }).toPass({ timeout: 2_000 }) + }) + } +) diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 48ffc52092..4a7ad30007 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -381,10 +381,17 @@ watch( * No `deep: true` needed — `nodeLocationProgressStates` is a computed that * returns a new `Record` object on every progress event (the underlying * `nodeProgressStates` ref is replaced wholesale by the WebSocket handler). + * + * `currentGraph` triggers this watcher on subgraph navigation so stale + * progress bars are cleared when returning to the root graph. */ watch( () => - [executionStore.nodeLocationProgressStates, canvasStore.canvas] as const, + [ + executionStore.nodeLocationProgressStates, + canvasStore.canvas, + canvasStore.currentGraph + ] as const, ([nodeLocationProgressStates, canvas]) => { if (!canvas?.graph) return for (const node of canvas.graph.nodes) {