Files
ComfyUI_frontend/browser_tests/tests/multiNodeExecution.spec.ts
2025-07-17 18:27:09 -07:00

386 lines
13 KiB
TypeScript

import { expect, mergeTests } from '@playwright/test'
import { comfyPageFixture } from '../fixtures/ComfyPage'
import { webSocketFixture } from '../fixtures/ws'
import {
ExecutionTestHelper,
PreviewTestHelper
} from '../helpers/ExecutionTestHelper'
const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('Multi-node Execution Progress', () => {
test.describe.configure({ mode: 'serial' })
let executionHelper: ExecutionTestHelper
let previewHelper: PreviewTestHelper
test.beforeEach(async ({ comfyPage }) => {
executionHelper = new ExecutionTestHelper(comfyPage.page)
previewHelper = new PreviewTestHelper(comfyPage.page)
})
test.afterEach(async () => {
if (executionHelper) {
await executionHelper.cleanup()
}
})
test('Can track progress of multiple async nodes executing in parallel', async ({
comfyPage,
ws
}) => {
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
// Get references to the async nodes
const sleepNode1 = await comfyPage.getNodeRefById(2)
const sleepNode2 = await comfyPage.getNodeRefById(3)
const progressNode = await comfyPage.getNodeRefById(4)
// Verify nodes are present
expect(sleepNode1).toBeDefined()
expect(sleepNode2).toBeDefined()
expect(progressNode).toBeDefined()
// Set up tracking for progress events
await executionHelper.setupEventTracking()
// Start execution
await comfyPage.queueButton.click()
// Wait for all three nodes (2, 3, 4) to show progress from real execution
const testId = executionHelper.getTestId()
await comfyPage.page.waitForFunction(
(testId) => {
const states = window[`__progressStates_${testId}`]
if (!states || states.length === 0) return false
const latestState = states[states.length - 1]
if (!latestState.nodes) return false
const node2 = latestState.nodes['2']
const node3 = latestState.nodes['3']
const node4 = latestState.nodes['4']
// Check that all nodes have started executing
return (
node2 &&
node2.state === 'running' &&
node3 &&
node3.state === 'running' &&
node4 &&
node4.state === 'running'
)
},
testId,
{ timeout: 10000 }
)
// Wait for progress to be applied to all nodes in the graph
await executionHelper.waitForGraphNodeProgress([2, 3, 4])
// Check that all nodes show progress
const nodeProgress1 = await sleepNode1.getProperty('progress')
const nodeProgress2 = await sleepNode2.getProperty('progress')
const nodeProgress3 = await progressNode.getProperty('progress')
// Progress values should now be defined (exact values depend on timing)
expect(nodeProgress1).toBeDefined()
expect(nodeProgress2).toBeDefined()
expect(nodeProgress3).toBeDefined()
expect(nodeProgress1).toBeGreaterThanOrEqual(0)
expect(nodeProgress1).toBeLessThanOrEqual(1)
expect(nodeProgress2).toBeGreaterThanOrEqual(0)
expect(nodeProgress2).toBeLessThanOrEqual(1)
expect(nodeProgress3).toBeGreaterThanOrEqual(0)
expect(nodeProgress3).toBeLessThanOrEqual(1)
// Wait for at least one node to finish
await executionHelper.waitForNodeFinish()
// Wait for the finished node's progress to be cleared
const testId2 = executionHelper.getTestId()
await comfyPage.page.waitForFunction(
(testId) => {
const states = window[`__progressStates_${testId}`]
if (!states || states.length === 0) return false
const latestState = states[states.length - 1]
if (!latestState.nodes) return false
// Find which nodes are finished
const finishedNodeIds = Object.entries(latestState.nodes)
.filter(([_, node]: [string, any]) => node.state === 'finished')
.map(([id, _]) => id)
// Check that finished nodes have no progress in the graph
return finishedNodeIds.some((id) => {
const node = window['app'].graph.getNodeById(parseInt(id))
return node && node.progress === undefined
})
},
testId2,
{ timeout: 5000 }
)
// Get current state of nodes
const testId3 = executionHelper.getTestId()
const currentState = await comfyPage.page.evaluate((testId) => {
const states = window[`__progressStates_${testId}`]
if (!states || states.length === 0) return null
const latestState = states[states.length - 1]
const graphNodes = {
'2': window['app'].graph.getNodeById(2),
'3': window['app'].graph.getNodeById(3),
'4': window['app'].graph.getNodeById(4)
}
return {
stateNodes: latestState.nodes,
graphProgress: {
'2': graphNodes['2']?.progress,
'3': graphNodes['3']?.progress,
'4': graphNodes['4']?.progress
}
}
}, testId3)
// Verify that finished nodes have no progress, running nodes have progress
if (currentState && currentState.stateNodes) {
Object.entries(currentState.stateNodes).forEach(
([nodeId, nodeState]: [string, any]) => {
const graphProgress = currentState.graphProgress[nodeId]
if (nodeState.state === 'finished') {
expect(graphProgress).toBeUndefined()
} else if (nodeState.state === 'running') {
expect(graphProgress).toBeDefined()
expect(graphProgress).toBeGreaterThanOrEqual(0)
expect(graphProgress).toBeLessThanOrEqual(1)
}
}
)
}
// Clean up by canceling execution
})
test('Updates visual state for multiple executing nodes', async ({
comfyPage,
ws
}) => {
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
// Wait for the graph to be properly initialized
await comfyPage.page.waitForFunction(
() => {
return window['app']?.graph?.nodes?.length > 0
},
{ timeout: 5000 }
)
// Set up tracking for progress events
await executionHelper.setupEventTracking()
// Start execution
await comfyPage.queueButton.click()
// Wait for multiple nodes to start executing
await executionHelper.waitForRunningNodes(2)
// Wait for the progress to be applied to nodes
await executionHelper.waitForGraphNodeProgress([2, 3])
// Verify that nodes have progress set (indicates they are executing)
const nodeStates = await comfyPage.page.evaluate(() => {
const node2 = window['app'].graph.getNodeById(2)
const node3 = window['app'].graph.getNodeById(3)
return {
node2Progress: node2?.progress,
node3Progress: node3?.progress,
// Check if any nodes are marked as running by having progress
hasRunningNodes:
(node2?.progress !== undefined && node2?.progress >= 0) ||
(node3?.progress !== undefined && node3?.progress >= 0)
}
})
expect(nodeStates.node2Progress).toBeDefined()
expect(nodeStates.node3Progress).toBeDefined()
expect(nodeStates.hasRunningNodes).toBe(true)
// Wait for at least one node to finish
await executionHelper.waitForNodeFinish()
// Wait for progress updates to reflect the finished state
const testId4 = executionHelper.getTestId()
await comfyPage.page.waitForFunction(
(testId) => {
const states = window[`__progressStates_${testId}`]
if (!states || states.length === 0) return false
const latestState = states[states.length - 1]
if (!latestState.nodes) return false
// Find nodes by their state
const finishedNodes = Object.entries(latestState.nodes)
.filter(([_, node]: [string, any]) => node.state === 'finished')
.map(([id, _]) => parseInt(id))
const runningNodes = Object.entries(latestState.nodes)
.filter(([_, node]: [string, any]) => node.state === 'running')
.map(([id, _]) => parseInt(id))
// Check graph nodes match the state
const allFinishedCorrect = finishedNodes.every((id) => {
const node = window['app'].graph.getNodeById(id)
return node && node.progress === undefined
})
const allRunningCorrect = runningNodes.every((id) => {
const node = window['app'].graph.getNodeById(id)
return node && node.progress !== undefined && node.progress >= 0
})
return (
allFinishedCorrect && allRunningCorrect && finishedNodes.length > 0
)
},
testId4,
{ timeout: 5000 }
)
// Verify the final node states
const testId5 = executionHelper.getTestId()
const finalNodeStates = await comfyPage.page.evaluate((testId) => {
const states = window[`__progressStates_${testId}`]
if (!states || states.length === 0) return null
const latestState = states[states.length - 1]
const node2 = window['app'].graph.getNodeById(2)
const node3 = window['app'].graph.getNodeById(3)
return {
node2State: latestState.nodes['2']?.state,
node3State: latestState.nodes['3']?.state,
node2Progress: node2?.progress,
node3Progress: node3?.progress
}
}, testId5)
// Verify finished nodes have no progress, running nodes have progress
if (finalNodeStates) {
if (finalNodeStates.node2State === 'finished') {
expect(finalNodeStates.node2Progress).toBeUndefined()
} else if (finalNodeStates.node2State === 'running') {
expect(finalNodeStates.node2Progress).toBeDefined()
}
if (finalNodeStates.node3State === 'finished') {
expect(finalNodeStates.node3Progress).toBeUndefined()
} else if (finalNodeStates.node3State === 'running') {
expect(finalNodeStates.node3Progress).toBeDefined()
}
}
})
test('Clears previews when nodes start executing', async ({
comfyPage,
ws
}) => {
await comfyPage.loadWorkflow('execution/parallel_async_nodes')
// Initialize tracking for revoked previews
await comfyPage.page.evaluate(() => {
window['__revokedNodes'] = []
})
// Set up some fake previews
await comfyPage.page.evaluate(() => {
window['app'].nodePreviewImages['2'] = ['fake-preview-url-1']
window['app'].nodePreviewImages['3'] = ['fake-preview-url-2']
})
// Verify previews exist
const previewsBefore = await comfyPage.page.evaluate(() => {
return {
node2: window['app'].nodePreviewImages['2'],
node3: window['app'].nodePreviewImages['3']
}
})
expect(previewsBefore.node2).toEqual(['fake-preview-url-1'])
expect(previewsBefore.node3).toEqual(['fake-preview-url-2'])
// Mock revokePreviews to track calls and set up event listeners
await previewHelper.setupPreviewTracking()
await executionHelper.setupEventTracking()
// Start real execution using command
await comfyPage.executeCommand('Comfy.QueuePrompt')
// Wait for execution to start
await executionHelper.waitForExecutionStart()
// Wait for real execution to trigger progress events that clear previews
const testId6 = executionHelper.getTestId()
await comfyPage.page.waitForFunction(
(testId) => {
const states = window[`__progressStates_${testId}`]
if (!states || states.length === 0) return false
// Check if we have progress for nodes 2 and 3
const hasNode2Progress = states.some(
(state: any) =>
state.nodes &&
state.nodes['2'] &&
state.nodes['2'].state === 'running'
)
const hasNode3Progress = states.some(
(state: any) =>
state.nodes &&
state.nodes['3'] &&
state.nodes['3'].state === 'running'
)
return hasNode2Progress && hasNode3Progress
},
testId6,
{ timeout: 10000 }
)
// Wait for the event to be processed and previews to be revoked
await comfyPage.page.waitForFunction(
() => {
const revokedNodes = window['__revokedNodes']
const node2PreviewCleared =
window['app'].nodePreviewImages['2'] === undefined
const node3PreviewCleared =
window['app'].nodePreviewImages['3'] === undefined
return (
revokedNodes.includes('2') &&
revokedNodes.includes('3') &&
node2PreviewCleared &&
node3PreviewCleared
)
},
{ timeout: 5000 }
)
// Check that revokePreviews was called for both nodes
const revokedNodes = await previewHelper.getRevokedNodes()
expect(revokedNodes).toContain('2')
expect(revokedNodes).toContain('3')
// Check that previews were cleared
const previewsAfter = await comfyPage.page.evaluate(() => {
return {
node2: window['app'].nodePreviewImages['2'],
node3: window['app'].nodePreviewImages['3']
}
})
expect(previewsAfter.node2).toBeUndefined()
expect(previewsAfter.node3).toBeUndefined()
})
})