test: stabilize flaky Playwright tests (#10817)

Stabilize flaky Playwright tests by improving test reliability.

This PR aims to identify and fix flaky e2e tests.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10817-test-stabilize-flaky-Playwright-tests-3366d73d365081ada40de73ce11af625)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-04-07 19:47:27 -07:00
committed by GitHub
parent d73c4406ed
commit 4cb83353cb
29 changed files with 760 additions and 536 deletions

View File

@@ -27,12 +27,12 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
})
test('Can undo multiple operations', async ({ comfyPage }) => {
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0)
await expect.poll(() => comfyPage.workflow.getUndoQueueSize()).toBe(0)
await expect.poll(() => comfyPage.workflow.getRedoQueueSize()).toBe(0)
// Save, confirm no errors & workflow modified flag removed
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
expect(await comfyPage.toast.getToastErrorCount()).toBe(0)
await expect.poll(() => comfyPage.toast.getToastErrorCount()).toBe(0)
await expect
.poll(() => comfyPage.workflow.isCurrentWorkflowModified())
.toBe(false)
@@ -164,7 +164,7 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
})
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
await expect.poll(() => comfyPage.workflow.getUndoQueueSize()).toBe(0)
await comfyPage.page.evaluate(() => {
window.app!.graph!.extra.foo = 'bar'
})
@@ -174,7 +174,7 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
})
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
await expect.poll(() => comfyPage.workflow.getUndoQueueSize()).toBe(0)
await comfyPage.canvasOps.pan({ x: 10, y: 10 })
await expect.poll(() => comfyPage.workflow.getUndoQueueSize()).toBe(0)
})

View File

@@ -38,8 +38,9 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
await comfyPage.clipboard.copy(null)
await comfyPage.clipboard.paste(null)
await comfyPage.clipboard.paste(null)
const resultString = await textBox.inputValue()
expect(resultString).toBe(originalString + originalString)
await expect
.poll(() => textBox.inputValue())
.toBe(originalString + originalString)
})
test('Can copy and paste widget value', async ({ comfyPage }) => {
@@ -114,20 +115,24 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
test('Can undo paste multiple nodes as single action', async ({
comfyPage
}) => {
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBeGreaterThan(1)
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(initialCount).toBeGreaterThan(1)
await comfyPage.canvas.click()
await comfyPage.keyboard.selectAll()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.clipboard.copy()
await comfyPage.clipboard.paste()
const pasteCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(pasteCount).toBe(initialCount * 2)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialCount * 2)
await comfyPage.keyboard.undo()
const undoCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(undoCount).toBe(initialCount)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialCount)
})
test(
@@ -135,7 +140,7 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
{ tag: ['@node'] },
async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('nodes/load_image_with_ksampler')
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(2)
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(2)
// Step 1: Copy a KSampler node with Ctrl+C and paste with Ctrl+V
const ksamplerNodes =
@@ -174,7 +179,7 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
{ timeout: 5_000 }
)
.toContain('image32x32')
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(3)
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(3)
// Step 3: Click empty canvas area, paste image → creates new LoadImage
await comfyPage.canvas.click({ position: { x: 50, y: 500 } })

View File

@@ -83,7 +83,7 @@ test.describe('Sign In dialog', { tag: '@ui' }, () => {
})
test('Should close dialog via close button', async () => {
await dialog.close()
await dialog.closeButton.click()
await expect(dialog.root).toBeHidden()
})

View File

@@ -48,10 +48,9 @@ test.describe('Menu', { tag: '@ui' }, () => {
await comfyPage.menu.topbar.saveWorkflow(workflowName)
expect(await comfyPage.menu.topbar.getTabNames()).toEqual([workflowName])
await comfyPage.menu.topbar.closeWorkflowTab(workflowName)
await comfyPage.nextFrame()
expect(await comfyPage.menu.topbar.getTabNames()).toEqual([
'Unsaved Workflow'
])
await expect
.poll(() => comfyPage.menu.topbar.getTabNames())
.toEqual(['Unsaved Workflow'])
})
})

View File

@@ -4,6 +4,7 @@ import {
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { fitToViewInstant } from '../helpers/fitToView'
import type { WorkspaceStore } from '../types/globals'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
// TODO: there might be a better solution for this
@@ -23,6 +24,67 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) {
await nodeRef.click('title')
}
async function openSelectionToolboxHelp(comfyPage: ComfyPage) {
await expect(comfyPage.selectionToolbox).toBeVisible()
const helpButton = comfyPage.selectionToolbox.getByTestId('info-button')
await expect(helpButton).toBeVisible()
await helpButton.click({ force: true })
await comfyPage.nextFrame()
return comfyPage.page.getByTestId('properties-panel')
}
async function setLocaleAndWaitForWorkflowReload(
comfyPage: ComfyPage,
locale: string
) {
await comfyPage.page.evaluate(async (targetLocale) => {
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow
if (!workflow) {
throw new Error('No active workflow while waiting for locale reload')
}
const changeTracker = workflow.changeTracker.constructor as unknown as {
isLoadingGraph: boolean
}
let sawLoading = false
const waitForReload = new Promise<void>((resolve, reject) => {
const timeoutAt = performance.now() + 5000
const tick = () => {
if (changeTracker.isLoadingGraph) {
sawLoading = true
}
if (sawLoading && !changeTracker.isLoadingGraph) {
resolve()
return
}
if (performance.now() > timeoutAt) {
reject(
new Error(
`Timed out waiting for workflow reload after setting locale to ${targetLocale}`
)
)
return
}
requestAnimationFrame(tick)
}
tick()
})
await window.app!.extensionManager.setting.set('Comfy.Locale', targetLocale)
await waitForReload
}, locale)
}
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
@@ -46,20 +108,8 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
// Select the node with panning to ensure toolbox is visible
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Wait for selection toolbox to appear
await expect(comfyPage.selectionToolbox).toBeVisible()
// Click the help button in the selection toolbox
const helpButton = comfyPage.selectionToolbox.locator(
'button[data-testid="info-button"]'
)
await expect(helpButton).toBeVisible()
await helpButton.click()
// Verify that the help page is shown for the correct node
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
await expect(helpPage).toContainText('KSampler')
await expect(helpPage.locator('.node-help-content')).toBeVisible()
})
@@ -168,16 +218,8 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Click help button
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
// Verify loading spinner is shown
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
await expect(helpPage.locator('.p-progressspinner')).toBeVisible()
// Wait for content to load
@@ -201,16 +243,8 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Click help button
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
// Verify fallback content is shown (description, inputs, outputs)
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
await expect(helpPage).toContainText('Description')
await expect(helpPage).toContainText('Inputs')
await expect(helpPage).toContainText('Outputs')
@@ -239,14 +273,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
await expect(helpPage).toContainText('KSampler Documentation')
// Check that relative image paths are prefixed correctly
@@ -290,14 +317,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
// Check relative video paths are prefixed
const relativeVideo = helpPage.locator('video[src*="demo.mp4"]')
@@ -364,15 +384,9 @@ This is documentation for a custom node.
await selectNodeWithPan(comfyPage, firstNode)
}
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
const helpButton = comfyPage.selectionToolbox.getByTestId('info-button')
if (await helpButton.isVisible()) {
await helpButton.click()
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
await expect(helpPage).toContainText('Custom Node Documentation')
// Check image path for custom nodes
@@ -408,14 +422,7 @@ This is documentation for a custom node.
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
// Dangerous elements should be removed
await expect(helpPage.locator('script')).toHaveCount(0)
@@ -471,27 +478,20 @@ This is English documentation.
})
// Set locale to Japanese
await comfyPage.settings.setSetting('Comfy.Locale', 'ja')
await setLocaleAndWaitForWorkflowReload(comfyPage, 'ja')
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
try {
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.waitFor({ state: 'visible', timeout: 10_000 })
await helpButton.click()
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
await expect(helpPage).toContainText('KSamplerード')
await expect(helpPage).toContainText('これは日本語のドキュメントです')
// Reset locale
await comfyPage.settings.setSetting('Comfy.Locale', 'en')
const helpPage = await openSelectionToolboxHelp(comfyPage)
await expect(helpPage).toContainText('KSamplerード')
await expect(helpPage).toContainText('これは日本語のドキュメントです')
} finally {
await setLocaleAndWaitForWorkflowReload(comfyPage, 'en')
}
})
test('Should handle network errors gracefully', async ({ comfyPage }) => {
@@ -505,14 +505,7 @@ This is English documentation.
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
// Should show fallback content (node description)
await expect(helpPage).toBeVisible()
@@ -552,14 +545,7 @@ This is English documentation.
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton.click()
const helpPage = comfyPage.page.locator(
'[data-testid="properties-panel"]'
)
const helpPage = await openSelectionToolboxHelp(comfyPage)
await expect(helpPage).toContainText('KSampler Help')
await expect(helpPage).toContainText('This is KSampler documentation')

View File

@@ -4,6 +4,16 @@ import {
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
async function waitForSearchInsertion(
comfyPage: ComfyPage,
initialNodeCount: number
) {
await expect(comfyPage.searchBox.input).toHaveCount(0)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialNodeCount + 1)
}
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
@@ -60,18 +70,22 @@ test.describe('Node search box', { tag: '@node' }, () => {
})
test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => {
const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.canvasOps.doubleClick()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await waitForSearchInsertion(comfyPage, initialNodeCount)
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
})
test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => {
const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.canvasOps.disconnectEdge()
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
suggestionIndex: 0
})
await waitForSearchInsertion(comfyPage, initialNodeCount)
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
})
@@ -81,6 +95,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.Graph.AutoPanSpeed', 0)
await comfyPage.workflow.loadWorkflow('links/batch_move_links')
const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
// Get the CLIP output slot (index 1) from the first CheckpointLoaderSimple node (id: 4)
const checkpointNode = await comfyPage.nodeOps.getNodeRefById(4)
@@ -98,6 +113,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
await waitForSearchInsertion(comfyPage, initialNodeCount)
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
@@ -108,12 +124,14 @@ test.describe('Node search box', { tag: '@node' }, () => {
'Link release connecting to node with no slots',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.canvasOps.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler', {
exact: true
})
await waitForSearchInsertion(comfyPage, initialNodeCount)
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)
@@ -296,11 +314,13 @@ test.describe('Release context menu', { tag: '@node' }, () => {
'Can search and add node from context menu',
{ tag: '@screenshot' },
async ({ comfyPage, comfyMouse }) => {
const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.canvasOps.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.contextMenu.clickMenuItem('Search')
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await waitForSearchInsertion(comfyPage, initialNodeCount)
await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png'
)

View File

@@ -16,13 +16,21 @@ test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
await expect(comfyPage.searchBox.input).toHaveCount(1)
// Search for and add the RecordAudio node
await comfyPage.searchBox.fillAndSelectFirstNode('RecordAudio')
await comfyPage.searchBox.fillAndSelectFirstNode('Record Audio', {
exact: true
})
await comfyPage.nextFrame()
// Verify the RecordAudio node was added
const recordAudioNodes =
await comfyPage.nodeOps.getNodeRefsByType('RecordAudio')
expect(recordAudioNodes.length).toBe(1)
await expect
.poll(
async () =>
(await comfyPage.nodeOps.getNodeRefsByType('RecordAudio')).length,
{
timeout: 5000
}
)
.toBe(1)
// Take a screenshot of the canvas with the RecordAudio node
await expect(comfyPage.canvas).toHaveScreenshot('record_audio_node.png')

View File

@@ -113,21 +113,48 @@ test.describe(
'reroute/single-native-reroute-default-workflow'
)
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
// since the link is a bezier curve and not a straight line.
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
const checkpointNode = await comfyPage.nodeOps.getNodeRefById(4)
const positiveClipNode = await comfyPage.nodeOps.getNodeRefById(6)
const negativeClipNode = await comfyPage.nodeOps.getNodeRefById(7)
const checkpointClipOutput = await checkpointNode.getOutput(1)
const positiveClipInput = await positiveClipNode.getInput(0)
const negativeClipInput = await negativeClipNode.getInput(0)
// Dynamically read the rendered link marker position from the canvas,
// targeting link 5 (CLIP from CheckpointLoaderSimple to negative CLIPTextEncode).
const middlePoint = await comfyPage.page.waitForFunction(() => {
const canvas = window['app']?.canvas
if (!canvas?.renderedPaths) return null
for (const segment of canvas.renderedPaths) {
if (segment.id === 5 && segment._pos) {
return { x: segment._pos[0], y: segment._pos[1] }
}
}
return null
})
const pos = await middlePoint.jsonValue()
if (!pos) throw new Error('Rendered midpoint for link 5 was not found')
// Click the middle point of the link to open the context menu.
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page.mouse.click(pos.x, pos.y)
// Click the "Delete" context menu option.
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_delete_from_midpoint_context_menu.png'
)
await expect
.poll(async () => ({
checkpointClipOutputLinks: await checkpointClipOutput.getLinkCount(),
positiveClipInputLinks: await positiveClipInput.getLinkCount(),
negativeClipInputLinks: await negativeClipInput.getLinkCount()
}))
.toEqual({
checkpointClipOutputLinks: 1,
positiveClipInputLinks: 1,
negativeClipInputLinks: 0
})
})
}
)

View File

@@ -229,6 +229,8 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
await cloneItem.click()
await expect(cloneItem).toHaveCount(0)
await comfyPage.nextFrame()
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(nodeCount + 1)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(nodeCount + 1)
})
})

View File

@@ -19,13 +19,17 @@ test.describe('@canvas Selection Rectangle', () => {
await comfyPage.canvas.press('Control+a')
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(totalCount)
await expect
.poll(() => comfyPage.vueNodes.getSelectedNodeCount())
.toBe(totalCount)
})
test('Click empty space deselects all', async ({ comfyPage }) => {
await comfyPage.canvas.press('Control+a')
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBeGreaterThan(0)
await expect
.poll(() => comfyPage.vueNodes.getSelectedNodeCount())
.toBeGreaterThan(0)
// Deselect by Ctrl+clicking the already-selected node (reliable cross-env)
await comfyPage.page
@@ -37,26 +41,26 @@ test.describe('@canvas Selection Rectangle', () => {
})
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
})
test('Single click selects one node', async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
})
test('Ctrl+click adds to selection', async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Empty Latent Image').click({
modifiers: ['Control']
})
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
})
test('Selected nodes have visual indicator', async ({ comfyPage }) => {
@@ -71,7 +75,7 @@ test.describe('@canvas Selection Rectangle', () => {
test('Drag-select rectangle selects multiple nodes', async ({
comfyPage
}) => {
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
// Use Ctrl+A to select all, which is functionally equivalent to
// drag-selecting the entire canvas and more reliable in CI
@@ -79,7 +83,9 @@ test.describe('@canvas Selection Rectangle', () => {
await comfyPage.nextFrame()
const totalCount = await comfyPage.vueNodes.getNodeCount()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(totalCount)
await expect
.poll(() => comfyPage.vueNodes.getSelectedNodeCount())
.toBe(totalCount)
expect(totalCount).toBeGreaterThan(1)
})
})

View File

@@ -1,16 +1,52 @@
import { expect } from '@playwright/test'
import type { ComfyPage } from '../../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
const bookmarksSettingId = 'Comfy.NodeLibrary.Bookmarks.V2'
const bookmarksCustomizationSettingId =
'Comfy.NodeLibrary.BookmarksCustomization'
type BookmarkCustomizationMap = Record<
string,
{
icon?: string
color?: string
}
>
async function expectBookmarks(comfyPage: ComfyPage, bookmarks: string[]) {
await expect
.poll(() => comfyPage.settings.getSetting<string[]>(bookmarksSettingId))
.toEqual(bookmarks)
}
async function expectBookmarkCustomization(
comfyPage: ComfyPage,
customization: BookmarkCustomizationMap
) {
await expect
.poll(() =>
comfyPage.settings.getSetting<BookmarkCustomizationMap>(
bookmarksCustomizationSettingId
)
)
.toEqual(customization)
}
async function renameInlineFolder(comfyPage: ComfyPage, newName: string) {
const renameInput = comfyPage.page.locator('.editable-text input')
await expect(renameInput).toBeVisible()
await renameInput.fill(newName)
await renameInput.press('Enter')
}
test.describe('Node library sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
await comfyPage.settings.setSetting(
'Comfy.NodeLibrary.BookmarksCustomization',
{}
)
await comfyPage.settings.setSetting(bookmarksSettingId, [])
await comfyPage.settings.setSetting(bookmarksCustomizationSettingId, {})
// Open the sidebar
const tab = comfyPage.menu.nodeLibraryTab
await tab.open()
@@ -21,14 +57,11 @@ test.describe('Node library sidebar', () => {
await tab.getFolder('sampling').click()
// Hover over a node to display the preview
const nodeSelector = '.p-tree-node-leaf'
const nodeSelector = tab.nodeSelector('KSampler (Advanced)')
await comfyPage.page.hover(nodeSelector)
// Verify the preview is displayed
const previewVisible = await comfyPage.page.isVisible(
'.node-lib-node-preview'
)
expect(previewVisible).toBe(true)
await expect(tab.nodePreview).toBeVisible()
const count = await comfyPage.nodeOps.getGraphNodesCount()
// Drag the node onto the canvas
@@ -48,9 +81,12 @@ test.describe('Node library sidebar', () => {
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, {
targetPosition
})
await comfyPage.nextFrame()
// Verify the node is added to the canvas
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(count + 1)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(count + 1)
})
test('Bookmark node', async ({ comfyPage }) => {
@@ -61,33 +97,29 @@ test.describe('Node library sidebar', () => {
await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click()
// Verify the bookmark is added to the bookmarks tab
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['KSamplerAdvanced'])
await expectBookmarks(comfyPage, ['KSamplerAdvanced'])
// Verify the bookmark node with the same name is added to the tree.
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
await expect(tab.getNode('KSampler (Advanced)')).toHaveCount(2)
// Hover on the bookmark node to display the preview
await comfyPage.page.hover('.node-lib-bookmark-tree-explorer .tree-leaf')
expect(await comfyPage.page.isVisible('.node-lib-node-preview')).toBe(true)
await expect(tab.nodePreview).toBeVisible()
})
test('Ignores unrecognized node', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo'])
await expectBookmarks(comfyPage, ['foo'])
await comfyPage.nextFrame()
const tab = comfyPage.menu.nodeLibraryTab
expect(await tab.getFolder('sampling').count()).toBe(1)
expect(await tab.getNode('foo').count()).toBe(0)
await expect(tab.getFolder('sampling')).toHaveCount(1)
await expect(tab.getNode('foo')).toHaveCount(0)
})
test('Displays empty bookmarks folder', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
expect(await tab.getFolder('foo').count()).toBe(1)
await expect(tab.getFolder('foo')).toHaveCount(1)
})
test('Can add new bookmark folder', async ({ comfyPage }) => {
@@ -97,17 +129,14 @@ test.describe('Node library sidebar', () => {
await textInput.waitFor({ state: 'visible' })
await textInput.fill('New Folder')
await textInput.press('Enter')
expect(await tab.getFolder('New Folder').count()).toBe(1)
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['New Folder/'])
await expect(tab.getFolder('New Folder')).toHaveCount(1)
await expectBookmarks(comfyPage, ['New Folder/'])
})
test('Can add nested bookmark folder', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByRole('menuitem', { name: 'New Folder' }).click()
@@ -116,59 +145,47 @@ test.describe('Node library sidebar', () => {
await textInput.fill('bar')
await textInput.press('Enter')
expect(await tab.getFolder('bar').count()).toBe(1)
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['foo/', 'foo/bar/'])
await expect(tab.getFolder('bar')).toHaveCount(1)
await expectBookmarks(comfyPage, ['foo/', 'foo/bar/'])
})
test('Can delete bookmark folder', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Delete').click()
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
await expectBookmarks(comfyPage, [])
})
test('Can rename bookmark folder', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page
.locator('.p-contextmenu-item-label:has-text("Rename")')
.click()
await comfyPage.page.keyboard.insertText('bar')
await comfyPage.page.keyboard.press('Enter')
await renameInlineFolder(comfyPage, 'bar')
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['bar/'])
await expectBookmarks(comfyPage, ['bar/'])
})
test('Can add bookmark by dragging node to bookmark folder', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('sampling').click()
await comfyPage.page.dragAndDrop(
tab.nodeSelector('KSampler (Advanced)'),
tab.folderSelector('foo')
)
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['foo/', 'foo/KSamplerAdvanced'])
await expectBookmarks(comfyPage, ['foo/', 'foo/KSamplerAdvanced'])
})
test('Can add bookmark by clicking bookmark button', async ({
@@ -177,41 +194,36 @@ test.describe('Node library sidebar', () => {
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('sampling').click()
await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click()
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['KSamplerAdvanced'])
await expectBookmarks(comfyPage, ['KSamplerAdvanced'])
})
test('Can unbookmark node (Top level bookmark)', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
await comfyPage.settings.setSetting(bookmarksSettingId, [
'KSamplerAdvanced'
])
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getNode('KSampler (Advanced)')).toHaveCount(1)
await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click()
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
await expectBookmarks(comfyPage, [])
})
test('Can unbookmark node (Library node bookmark)', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
await comfyPage.settings.setSetting(bookmarksSettingId, [
'KSamplerAdvanced'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('sampling').click()
await expect(tab.getNode('KSampler (Advanced)')).toHaveCount(2)
await tab
.getNodeInFolder('KSampler (Advanced)', 'sampling')
.locator('.bookmark-button')
.click()
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
await expectBookmarks(comfyPage, [])
})
test('Can customize icon', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Customize').click()
const dialog = comfyPage.page.getByRole('dialog', {
@@ -228,11 +240,7 @@ test.describe('Node library sidebar', () => {
await colorGroup.getByRole('button').nth(1).click()
await dialog.getByRole('button', { name: 'Confirm' }).click()
await comfyPage.nextFrame()
expect(
await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
).toEqual({
await expectBookmarkCustomization(comfyPage, {
'foo/': {
icon: 'pi-folder',
color: '#007bff'
@@ -241,10 +249,9 @@ test.describe('Node library sidebar', () => {
})
// If color is left as default, it should not be saved
test('Can customize icon (default field)', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Customize').click()
const dialog = comfyPage.page.getByRole('dialog', {
@@ -255,11 +262,7 @@ test.describe('Node library sidebar', () => {
await iconGroup.getByRole('button').nth(1).click()
await dialog.getByRole('button', { name: 'Confirm' }).click()
await comfyPage.nextFrame()
expect(
await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
).toEqual({
await expectBookmarkCustomization(comfyPage, {
'foo/': {
icon: 'pi-folder'
}
@@ -270,10 +273,9 @@ test.describe('Node library sidebar', () => {
comfyPage
}) => {
// Open customization dialog
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Customize').click()
@@ -302,84 +304,76 @@ test.describe('Node library sidebar', () => {
await comfyPage.nextFrame()
// Verify the color selection is saved
const setting = await comfyPage.settings.getSetting<
Record<string, { icon?: string; color?: string }>
>('Comfy.NodeLibrary.BookmarksCustomization')
await expect(setting).toHaveProperty(['foo/', 'color'])
await expect(setting['foo/'].color).not.toBeNull()
await expect(setting['foo/'].color).not.toBeUndefined()
await expect(setting['foo/'].color).not.toBe('')
await expect
.poll(async () => {
return (
(
await comfyPage.settings.getSetting<BookmarkCustomizationMap>(
bookmarksCustomizationSettingId
)
)['foo/']?.color ?? ''
)
})
.toMatch(/^#.+/)
})
test('Can rename customized bookmark folder', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(
'Comfy.NodeLibrary.BookmarksCustomization',
{
'foo/': {
icon: 'pi-folder',
color: '#007bff'
}
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
await comfyPage.settings.setSetting(bookmarksCustomizationSettingId, {
'foo/': {
icon: 'pi-folder',
color: '#007bff'
}
)
})
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page
.locator('.p-contextmenu-item-label:has-text("Rename")')
.click()
await comfyPage.page.keyboard.insertText('bar')
await comfyPage.page.keyboard.press('Enter')
await renameInlineFolder(comfyPage, 'bar')
await comfyPage.nextFrame()
await expect(async () => {
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['bar/'])
expect(
await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
).toEqual({
'bar/': {
icon: 'pi-folder',
color: '#007bff'
await expect
.poll(async () => {
return {
bookmarks:
await comfyPage.settings.getSetting<string[]>(bookmarksSettingId),
customization:
await comfyPage.settings.getSetting<BookmarkCustomizationMap>(
bookmarksCustomizationSettingId
)
}
})
.toEqual({
bookmarks: ['bar/'],
customization: {
'bar/': {
icon: 'pi-folder',
color: '#007bff'
}
}
})
}).toPass({
timeout: 2_000
})
})
test('Can delete customized bookmark folder', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(
'Comfy.NodeLibrary.BookmarksCustomization',
{
'foo/': {
icon: 'pi-folder',
color: '#007bff'
}
await comfyPage.settings.setSetting(bookmarksSettingId, ['foo/'])
await comfyPage.settings.setSetting(bookmarksCustomizationSettingId, {
'foo/': {
icon: 'pi-folder',
color: '#007bff'
}
)
})
const tab = comfyPage.menu.nodeLibraryTab
await expect(tab.getFolder('foo')).toBeVisible()
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Delete').click()
await comfyPage.nextFrame()
expect(
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
expect(
await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
).toEqual({})
await expectBookmarks(comfyPage, [])
await expectBookmarkCustomization(comfyPage, {})
})
test('Can filter nodes in both trees', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
await comfyPage.settings.setSetting(bookmarksSettingId, [
'foo/',
'foo/KSamplerAdvanced',
'KSampler'

View File

@@ -112,17 +112,17 @@ test.describe('Subgraph Navigation', { tag: ['@slow', '@subgraph'] }, () => {
await subgraphNode.navigateIntoSubgraph()
await comfyPage.nextFrame()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect(comfyPage.page.locator(SELECTORS.breadcrumb)).toBeVisible()
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.nextFrame()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(false)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(false)
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.nextFrame()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(false)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(false)
})
test('Breadcrumb disappears after switching workflows while inside subgraph', async ({
@@ -194,7 +194,7 @@ test.describe('Subgraph Navigation', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
// Verify we're in a subgraph
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
// Test that Escape no longer exits subgraph
await comfyPage.page.keyboard.press('Escape')
@@ -206,7 +206,7 @@ test.describe('Subgraph Navigation', { tag: ['@slow', '@subgraph'] }, () => {
// Test that Alt+Q now exits subgraph
await comfyPage.page.keyboard.press('Alt+q')
await comfyPage.nextFrame()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(false)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(false)
})
test('Escape prioritizes closing dialogs over exiting subgraph', async ({
@@ -240,12 +240,12 @@ test.describe('Subgraph Navigation', { tag: ['@slow', '@subgraph'] }, () => {
).not.toBeVisible()
// Should still be in subgraph
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
// Press Escape again - now should exit subgraph
await comfyPage.page.keyboard.press('Escape')
await comfyPage.nextFrame()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(false)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(false)
})
})
@@ -372,7 +372,7 @@ test.describe('Subgraph Navigation', { tag: ['@slow', '@subgraph'] }, () => {
await subgraphNode.navigateIntoSubgraph()
// Verify we're inside the subgraph
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
// Navigate back to the root graph
await comfyPage.page.keyboard.press('Escape')
@@ -410,7 +410,7 @@ test.describe('Subgraph Navigation', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nodeOps.getNodeRefById(subgraphNodeId)
await subgraphNode.navigateIntoSubgraph()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.nextFrame()

View File

@@ -41,8 +41,9 @@ test.describe(
await comfyPage.page.keyboard.press('Control+v')
await comfyPage.nextFrame()
const finalNodeCount = await comfyPage.subgraph.getNodeCount()
expect(finalNodeCount).toBe(initialNodeCount + 1)
await expect
.poll(() => comfyPage.subgraph.getNodeCount())
.toBe(initialNodeCount + 1)
})
test('Can undo and redo operations in subgraph', async ({ comfyPage }) => {
@@ -63,15 +64,17 @@ test.describe(
await comfyPage.keyboard.undo()
await comfyPage.nextFrame()
const afterUndoCount = await comfyPage.subgraph.getNodeCount()
expect(afterUndoCount).toBe(initialCount - 1)
await expect
.poll(() => comfyPage.subgraph.getNodeCount())
.toBe(initialCount - 1)
// Redo
await comfyPage.keyboard.redo()
await comfyPage.nextFrame()
const afterRedoCount = await comfyPage.subgraph.getNodeCount()
expect(afterRedoCount).toBe(initialCount)
await expect
.poll(() => comfyPage.subgraph.getNodeCount())
.toBe(initialCount)
})
}
)

View File

@@ -1,5 +1,6 @@
import { expect } from '@playwright/test'
import type { ComfyPage } from '../../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { TestIds } from '../../fixtures/selectors'
import { fitToViewInstant } from '../../helpers/fitToView'
@@ -8,6 +9,30 @@ import {
getPromotedWidgetCount
} from '../../helpers/promotedWidgets'
async function expectPromotedWidgetNamesToContain(
comfyPage: ComfyPage,
nodeId: string,
widgetName: string
) {
await expect
.poll(() => getPromotedWidgetNames(comfyPage, nodeId), {
timeout: 5000
})
.toContain(widgetName)
}
async function expectPromotedWidgetCountToBeGreaterThan(
comfyPage: ComfyPage,
nodeId: string,
count: number
) {
await expect
.poll(() => getPromotedWidgetCount(comfyPage, nodeId), {
timeout: 5000
})
.toBeGreaterThan(count)
}
test.describe(
'Subgraph Widget Promotion',
{ tag: ['@subgraph', '@widget'] },
@@ -34,12 +59,10 @@ test.describe(
// The KSampler has a "seed" widget which is in the recommended list.
// The promotion store should have at least the seed widget promoted.
const nodeId = String(subgraphNode.id)
const promotedNames = await getPromotedWidgetNames(comfyPage, nodeId)
expect(promotedNames).toContain('seed')
await expectPromotedWidgetNamesToContain(comfyPage, nodeId, 'seed')
// SubgraphNode should have widgets (promoted views)
const widgetCount = await getPromotedWidgetCount(comfyPage, nodeId)
expect(widgetCount).toBeGreaterThan(0)
await expectPromotedWidgetCountToBeGreaterThan(comfyPage, nodeId, 0)
})
test('CLIPTextEncode text widget is auto-promoted', async ({
@@ -54,12 +77,9 @@ test.describe(
await comfyPage.nextFrame()
const nodeId = String(subgraphNode.id)
const promotedNames = await getPromotedWidgetNames(comfyPage, nodeId)
expect(promotedNames.length).toBeGreaterThan(0)
// CLIPTextEncode is in the recommendedNodes list, so its text widget
// should be promoted
expect(promotedNames).toContain('text')
await expectPromotedWidgetNamesToContain(comfyPage, nodeId, 'text')
})
test('SaveImage/PreviewImage nodes get pseudo-widget promoted', async ({
@@ -75,13 +95,12 @@ test.describe(
const subgraphNode = await saveNode.convertToSubgraph()
await comfyPage.nextFrame()
const promotedNames = await getPromotedWidgetNames(
comfyPage,
String(subgraphNode.id)
)
// SaveImage is in the recommendedNodes list, so filename_prefix is promoted
expect(promotedNames).toContain('filename_prefix')
await expectPromotedWidgetNamesToContain(
comfyPage,
String(subgraphNode.id),
'filename_prefix'
)
})
})
@@ -160,7 +179,7 @@ test.describe(
await comfyPage.vueNodes.enterSubgraph('11')
await comfyPage.nextFrame()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
})
test('Multiple promoted widgets render on SubgraphNode in Vue mode', async ({
@@ -315,8 +334,7 @@ test.describe(
await comfyPage.subgraph.exitViaBreadcrumb()
// SubgraphNode should now have the promoted widget
const widgetCount = await getPromotedWidgetCount(comfyPage, '2')
expect(widgetCount).toBeGreaterThan(0)
await expectPromotedWidgetCountToBeGreaterThan(comfyPage, '2', 0)
})
test('Can un-promote a widget from inside a subgraph', async ({
@@ -352,8 +370,8 @@ test.describe(
await comfyPage.nextFrame()
await comfyPage.nextFrame()
await expectPromotedWidgetCountToBeGreaterThan(comfyPage, '2', 0)
const initialWidgetCount = await getPromotedWidgetCount(comfyPage, '2')
expect(initialWidgetCount).toBeGreaterThan(0)
// Navigate back in and un-promote
const subgraphNode2 = await comfyPage.nodeOps.getNodeRefById('2')
@@ -382,8 +400,11 @@ test.describe(
await comfyPage.subgraph.exitViaBreadcrumb()
// SubgraphNode should have fewer widgets
const finalWidgetCount = await getPromotedWidgetCount(comfyPage, '2')
expect(finalWidgetCount).toBeLessThan(initialWidgetCount)
await expect
.poll(() => getPromotedWidgetCount(comfyPage, '2'), {
timeout: 5000
})
.toBeLessThan(initialWidgetCount)
})
})
@@ -451,9 +472,11 @@ test.describe(
// The SaveImage node is in the recommendedNodes list, so its
// filename_prefix widget should be auto-promoted
const promotedNames = await getPromotedWidgetNames(comfyPage, '5')
expect(promotedNames.length).toBeGreaterThan(0)
expect(promotedNames).toContain('filename_prefix')
await expectPromotedWidgetNamesToContain(
comfyPage,
'5',
'filename_prefix'
)
})
test('Converting SaveImage to subgraph promotes its widgets', async ({
@@ -471,11 +494,12 @@ test.describe(
// SaveImage is a recommended node, so filename_prefix should be promoted
const nodeId = String(subgraphNode.id)
const promotedNames = await getPromotedWidgetNames(comfyPage, nodeId)
expect(promotedNames.length).toBeGreaterThan(0)
const widgetCount = await getPromotedWidgetCount(comfyPage, nodeId)
expect(widgetCount).toBeGreaterThan(0)
await expectPromotedWidgetNamesToContain(
comfyPage,
nodeId,
'filename_prefix'
)
await expectPromotedWidgetCountToBeGreaterThan(comfyPage, nodeId, 0)
})
})
@@ -600,8 +624,8 @@ test.describe(
)
await comfyPage.nextFrame()
await expectPromotedWidgetCountToBeGreaterThan(comfyPage, '5', 0)
const initialNames = await getPromotedWidgetNames(comfyPage, '5')
expect(initialNames.length).toBeGreaterThan(0)
const outerSubgraph = await comfyPage.nodeOps.getNodeRefById('5')
await outerSubgraph.navigateIntoSubgraph()
@@ -617,13 +641,16 @@ test.describe(
await comfyPage.subgraph.exitViaBreadcrumb()
const finalNames = await getPromotedWidgetNames(comfyPage, '5')
const expectedNames = [...initialNames]
const removedIndex = expectedNames.indexOf(removedSlotName!)
expect(removedIndex).toBeGreaterThanOrEqual(0)
expectedNames.splice(removedIndex, 1)
expect(finalNames).toEqual(expectedNames)
await expect
.poll(() => getPromotedWidgetNames(comfyPage, '5'), {
timeout: 5000
})
.toEqual(expectedNames)
})
test('Removing I/O slot removes associated promoted widget', async ({
@@ -635,8 +662,13 @@ test.describe(
'subgraphs/subgraph-with-promoted-text-widget'
)
const initialWidgetCount = await getPromotedWidgetCount(comfyPage, '11')
expect(initialWidgetCount).toBeGreaterThan(0)
let initialWidgetCount = 0
await expect
.poll(() => getPromotedWidgetCount(comfyPage, '11'), {
timeout: 5000
})
.toBeGreaterThan(0)
initialWidgetCount = await getPromotedWidgetCount(comfyPage, '11')
// Navigate into subgraph
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
@@ -649,8 +681,11 @@ test.describe(
await comfyPage.subgraph.exitViaBreadcrumb()
// Widget count should be reduced
const finalWidgetCount = await getPromotedWidgetCount(comfyPage, '11')
expect(finalWidgetCount).toBeLessThan(initialWidgetCount)
await expect
.poll(() => getPromotedWidgetCount(comfyPage, '11'), {
timeout: 5000
})
.toBeLessThan(initialWidgetCount)
})
})
}

View File

@@ -190,11 +190,13 @@ test.describe(
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
const subgraphNodeId = String(subgraphNode.id)
const promotedNames = await getPromotedWidgetNames(
comfyPage,
subgraphNodeId
)
expect(promotedNames).toContain('seed')
await expect(async () => {
const promotedNames = await getPromotedWidgetNames(
comfyPage,
subgraphNodeId
)
expect(promotedNames).toContain('seed')
}).toPass({ timeout: 5000 })
// Wait for Vue nodes to render
await comfyPage.vueNodes.waitForNodes()

View File

@@ -334,12 +334,12 @@ test.describe('Subgraph Serialization', { tag: ['@subgraph'] }, () => {
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('5')
await subgraphNode.navigateIntoSubgraph()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(true)
await comfyPage.page.keyboard.press('Escape')
await comfyPage.nextFrame()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(false)
await expect.poll(() => comfyPage.subgraph.isInSubgraph()).toBe(false)
})
})

View File

@@ -48,8 +48,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.subgraph.connectFromInput(vaeEncodeNode, 0)
await comfyPage.nextFrame()
const finalCount = await comfyPage.subgraph.getSlotCount('input')
expect(finalCount).toBe(initialCount + 1)
await expect
.poll(() => comfyPage.subgraph.getSlotCount('input'))
.toBe(initialCount + 1)
})
test('Can add output slots to subgraph', async ({ comfyPage }) => {
@@ -67,8 +68,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.subgraph.connectToOutput(vaeEncodeNode, 0)
await comfyPage.nextFrame()
const finalCount = await comfyPage.subgraph.getSlotCount('output')
expect(finalCount).toBe(initialCount + 1)
await expect
.poll(() => comfyPage.subgraph.getSlotCount('output'))
.toBe(initialCount + 1)
})
test('Can remove input slots from subgraph', async ({ comfyPage }) => {
@@ -86,8 +88,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
await comfyPage.nextFrame()
const finalCount = await comfyPage.subgraph.getSlotCount('input')
expect(finalCount).toBe(initialCount - 1)
await expect
.poll(() => comfyPage.subgraph.getSlotCount('input'))
.toBe(initialCount - 1)
})
test('Can remove output slots from subgraph', async ({ comfyPage }) => {
@@ -105,8 +108,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
await comfyPage.nextFrame()
const finalCount = await comfyPage.subgraph.getSlotCount('output')
expect(finalCount).toBe(initialCount - 1)
await expect
.poll(() => comfyPage.subgraph.getSlotCount('output'))
.toBe(initialCount - 1)
})
})
@@ -135,10 +139,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
await comfyPage.nextFrame()
const newInputName = await comfyPage.subgraph.getSlotLabel('input')
expect(newInputName).toBe(RENAMED_INPUT_NAME)
expect(newInputName).not.toBe(initialInputLabel)
await expect
.poll(() => comfyPage.subgraph.getSlotLabel('input'))
.toBe(RENAMED_INPUT_NAME)
})
test('Can rename input slots via double-click', async ({ comfyPage }) => {
@@ -161,10 +164,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
await comfyPage.nextFrame()
const newInputName = await comfyPage.subgraph.getSlotLabel('input')
expect(newInputName).toBe(RENAMED_INPUT_NAME)
expect(newInputName).not.toBe(initialInputLabel)
await expect
.poll(() => comfyPage.subgraph.getSlotLabel('input'))
.toBe(RENAMED_INPUT_NAME)
})
test('Can rename output slots via double-click', async ({ comfyPage }) => {
@@ -188,10 +190,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
await comfyPage.nextFrame()
const newOutputName = await comfyPage.subgraph.getSlotLabel('output')
expect(newOutputName).toBe(renamedOutputName)
expect(newOutputName).not.toBe(initialOutputLabel)
await expect
.poll(() => comfyPage.subgraph.getSlotLabel('output'))
.toBe(renamedOutputName)
})
test('Right-click context menu still works alongside double-click', async ({
@@ -220,10 +221,9 @@ test.describe('Subgraph Slots', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
await comfyPage.nextFrame()
const newInputName = await comfyPage.subgraph.getSlotLabel('input')
expect(newInputName).toBe(rightClickRenamedName)
expect(newInputName).not.toBe(initialInputLabel)
await expect
.poll(() => comfyPage.subgraph.getSlotLabel('input'))
.toBe(rightClickRenamedName)
})
test('Can double-click on slot label text to rename', async ({

View File

@@ -97,9 +97,9 @@ test.describe('Vue Node Context Menu', () => {
})
await comfyPage.nextFrame()
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(
initialCount + 1
)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialCount + 1)
})
test('should duplicate node via context menu', async ({ comfyPage }) => {
@@ -108,9 +108,9 @@ test.describe('Vue Node Context Menu', () => {
await openContextMenu(comfyPage, 'Load Checkpoint')
await clickExactMenuItem(comfyPage, 'Duplicate')
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(
initialCount + 1
)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialCount + 1)
})
test('should pin and unpin node via context menu', async ({
@@ -125,7 +125,7 @@ test.describe('Vue Node Context Menu', () => {
const fixture = await comfyPage.vueNodes.getFixtureByTitle(nodeTitle)
await expect(fixture.pinIndicator).toBeVisible()
expect(await nodeRef.isPinned()).toBe(true)
await expect.poll(() => nodeRef.isPinned()).toBe(true)
// Verify drag blocked
const header = fixture.header
@@ -143,7 +143,7 @@ test.describe('Vue Node Context Menu', () => {
await clickExactMenuItem(comfyPage, 'Unpin')
await expect(fixture.pinIndicator).not.toBeVisible()
expect(await nodeRef.isPinned()).toBe(false)
await expect.poll(() => nodeRef.isPinned()).toBe(false)
})
test('should bypass node and remove bypass via context menu', async ({
@@ -155,7 +155,7 @@ test.describe('Vue Node Context Menu', () => {
await openContextMenu(comfyPage, nodeTitle)
await clickExactMenuItem(comfyPage, 'Bypass')
expect(await nodeRef.isBypassed()).toBe(true)
await expect.poll(() => nodeRef.isBypassed()).toBe(true)
await expect(getNodeWrapper(comfyPage, nodeTitle)).toHaveClass(
BYPASS_CLASS
)
@@ -163,7 +163,7 @@ test.describe('Vue Node Context Menu', () => {
await openContextMenu(comfyPage, nodeTitle)
await clickExactMenuItem(comfyPage, 'Remove Bypass')
expect(await nodeRef.isBypassed()).toBe(false)
await expect.poll(() => nodeRef.isBypassed()).toBe(false)
await expect(getNodeWrapper(comfyPage, nodeTitle)).not.toHaveClass(
BYPASS_CLASS
)
@@ -206,6 +206,26 @@ test.describe('Vue Node Context Menu', () => {
.grantPermissions(['clipboard-read', 'clipboard-write'])
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
await comfyPage.vueNodes.waitForNodes(1)
await comfyPage.page
.locator('[data-node-id] img')
.first()
.waitFor({ state: 'visible' })
const [loadImageNode] =
await comfyPage.nodeOps.getNodeRefsByTitle('Load Image')
if (!loadImageNode) throw new Error('Load Image node not found')
await expect
.poll(
() =>
comfyPage.page.evaluate(
(nodeId) =>
window.app!.graph.getNodeById(nodeId)?.imgs?.length ?? 0,
loadImageNode.id
),
{ timeout: 5_000 }
)
.toBeGreaterThan(0)
})
test('should copy image to clipboard via context menu', async ({
@@ -215,13 +235,16 @@ test.describe('Vue Node Context Menu', () => {
await clickExactMenuItem(comfyPage, 'Copy Image')
// Verify the clipboard contains an image
const hasImage = await comfyPage.page.evaluate(async () => {
const items = await navigator.clipboard.read()
return items.some((item) =>
item.types.some((t) => t.startsWith('image/'))
)
})
expect(hasImage).toBe(true)
await expect
.poll(async () => {
return comfyPage.page.evaluate(async () => {
const items = await navigator.clipboard.read()
return items.some((item) =>
item.types.some((t) => t.startsWith('image/'))
)
})
})
.toBe(true)
})
test('should paste image to LoadImage node via context menu', async ({
@@ -374,9 +397,9 @@ test.describe('Vue Node Context Menu', () => {
})
await comfyPage.nextFrame()
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(
initialCount + nodeTitles.length
)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialCount + nodeTitles.length)
})
test('should duplicate selected nodes via context menu', async ({
@@ -387,9 +410,9 @@ test.describe('Vue Node Context Menu', () => {
await openMultiNodeContextMenu(comfyPage, nodeTitles)
await clickExactMenuItem(comfyPage, 'Duplicate')
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(
initialCount + nodeTitles.length
)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialCount + nodeTitles.length)
})
test('should pin and unpin selected nodes via context menu', async ({
@@ -420,7 +443,7 @@ test.describe('Vue Node Context Menu', () => {
for (const title of nodeTitles) {
const nodeRef = await getNodeRef(comfyPage, title)
expect(await nodeRef.isBypassed()).toBe(true)
await expect.poll(() => nodeRef.isBypassed()).toBe(true)
await expect(getNodeWrapper(comfyPage, title)).toHaveClass(BYPASS_CLASS)
}
@@ -429,7 +452,7 @@ test.describe('Vue Node Context Menu', () => {
for (const title of nodeTitles) {
const nodeRef = await getNodeRef(comfyPage, title)
expect(await nodeRef.isBypassed()).toBe(false)
await expect.poll(() => nodeRef.isBypassed()).toBe(false)
await expect(getNodeWrapper(comfyPage, title)).not.toHaveClass(
BYPASS_CLASS
)
@@ -501,9 +524,9 @@ test.describe('Vue Node Context Menu', () => {
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
await expect(subgraphNode).toBeVisible()
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(
initialCount - nodeTitles.length + 1
)
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(initialCount - nodeTitles.length + 1)
})
})
})

View File

@@ -29,23 +29,16 @@ test.describe('Vue Node Moving', () => {
expect(diffY).toBeGreaterThan(0)
}
test(
'should allow moving nodes by dragging',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const loadCheckpointHeaderPos =
await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.canvasOps.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
y: 256
})
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.canvasOps.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
y: 256
})
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
}
)
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
})
test('should not move node when pointer moves less than drag threshold', async ({
comfyPage

View File

@@ -24,40 +24,43 @@ test.describe('Vue Node Selection', () => {
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Empty Latent Image').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
await comfyPage.page.getByText('KSampler').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(3)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(3)
})
test(`should allow de-selecting nodes with ${name}+click`, async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Load Checkpoint').click({
modifiers: [modifier]
})
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
})
}
test('should select all nodes with ctrl+a', async ({ comfyPage }) => {
await expect
.poll(() => comfyPage.vueNodes.getNodeCount())
.toBeGreaterThan(0)
const initialCount = await comfyPage.vueNodes.getNodeCount()
expect(initialCount).toBeGreaterThan(0)
await comfyPage.canvas.press('Control+a')
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
expect(selectedCount).toBe(initialCount)
await expect
.poll(() => comfyPage.vueNodes.getSelectedNodeCount())
.toBe(initialCount)
})
test('should select pinned node without dragging', async ({ comfyPage }) => {
@@ -73,7 +76,7 @@ test.describe('Vue Node Selection', () => {
const pinIndicator = checkpointNode.locator(PIN_INDICATOR)
await expect(pinIndicator).toBeVisible()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
const initialPos = await checkpointNodeHeader.boundingBox()
if (!initialPos) throw new Error('Failed to get header position')
@@ -87,6 +90,6 @@ test.describe('Vue Node Selection', () => {
if (!finalPos) throw new Error('Failed to get header position after drag')
expect(finalPos).toEqual(initialPos)
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await expect.poll(() => comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
})
})

View File

@@ -84,24 +84,25 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Should refresh combo values of nodes with v2 combo input spec', async ({
comfyPage
}) => {
const getComboValues = async () =>
comfyPage.page.evaluate(() => {
return window
.app!.graph!.nodes.find(
(node) => node.title === 'Node With V2 Combo Input'
)!
.widgets!.find((widget) => widget.name === 'combo_input')!.options
.values
})
await comfyPage.workflow.loadWorkflow('inputs/node_with_v2_combo_input')
// click canvas to focus
await comfyPage.page.mouse.click(400, 300)
// press R to trigger refresh
await comfyPage.page.keyboard.press('r')
// wait for nodes' widgets to be updated
await comfyPage.page.mouse.click(400, 300)
await comfyPage.nextFrame()
// get the combo widget's values
const comboValues = await comfyPage.page.evaluate(() => {
return window
.app!.graph!.nodes.find(
(node) => node.title === 'Node With V2 Combo Input'
)!
.widgets!.find((widget) => widget.name === 'combo_input')!.options
.values
})
expect(comboValues).toEqual(['A', 'B'])
await expect
.poll(() => getComboValues(), { timeout: 5_000 })
.toEqual(['A', 'B'])
})
})
@@ -125,6 +126,7 @@ test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
const widget = await node.getWidget(0)
await comfyPage.page.evaluate(() => {
window.widgetValue = undefined
const widget = window.app!.graph!.nodes[0].widgets![0]
widget.callback = (value: number) => {
window.widgetValue = value
@@ -133,9 +135,11 @@ test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
await widget.dragHorizontal(50)
await expect(comfyPage.canvas).toHaveScreenshot('slider_widget_dragged.png')
expect(
await comfyPage.page.evaluate(() => window.widgetValue)
).toBeDefined()
await expect
.poll(() => comfyPage.page.evaluate(() => window.widgetValue), {
timeout: 2_000
})
.toBeDefined()
})
})
@@ -146,6 +150,7 @@ test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
const widget = await node.getWidget(0)
await comfyPage.page.evaluate(() => {
window.widgetValue = undefined
const widget = window.app!.graph!.nodes[0].widgets![0]
widget.callback = (value: number) => {
window.widgetValue = value
@@ -154,9 +159,11 @@ test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
await widget.dragHorizontal(50)
await expect(comfyPage.canvas).toHaveScreenshot('seed_widget_dragged.png')
expect(
await comfyPage.page.evaluate(() => window.widgetValue)
).toBeDefined()
await expect
.poll(() => comfyPage.page.evaluate(() => window.widgetValue), {
timeout: 2_000
})
.toBeDefined()
})
})
@@ -209,8 +216,7 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
// Expect the filename combo value to be updated
const fileComboWidget = await loadImageNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toBe('image32x32.webp')
await expect.poll(() => fileComboWidget.getValue()).toBe('image32x32.webp')
})
test('Can change image by changing the filename combo value', async ({
@@ -248,8 +254,7 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
)
// Expect the filename combo value to be updated
const filename = await fileComboWidget.getValue()
expect(filename).toBe('image32x32.webp')
await expect.poll(() => fileComboWidget.getValue()).toBe('image32x32.webp')
})
test('Displays buttons when viewing single image of batch', async ({
comfyPage
@@ -301,8 +306,9 @@ test.describe(
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toContain('animated_webp.webp')
await expect
.poll(() => fileComboWidget.getValue())
.toContain('animated_webp.webp')
})
test('Can preview saved animated webp image', async ({ comfyPage }) => {
@@ -317,9 +323,20 @@ test.describe(
// Drag and drop image file onto the load animated webp node
await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
dropPosition: { x, y },
waitForUpload: true
})
await comfyPage.nextFrame()
await expect
.poll(
() =>
comfyPage.page.evaluate(
(loadId) => window.app!.nodeOutputs[loadId]?.images?.length ?? 0,
loadAnimatedWebpNode.id
),
{ timeout: 10_000 }
)
.toBeGreaterThan(0)
// Get the SaveAnimatedWEBP node
const saveNodes =
@@ -337,11 +354,23 @@ test.describe(
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await comfyPage.nextFrame()
await comfyPage.nextFrame()
await expect(
comfyPage.page.locator('.dom-widget').locator('img')
).toHaveCount(2, { timeout: 10_000 })
await expect
.poll(
() =>
comfyPage.page.evaluate(
([loadId, saveId]) => {
const graph = window.app!.graph
return [loadId, saveId].map(
(nodeId) => (graph.getNodeById(nodeId)?.imgs?.length ?? 0) > 0
)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
),
{ timeout: 10_000 }
)
.toEqual([true, true])
})
}
)

View File

@@ -2,8 +2,45 @@ import { readFileSync } from 'fs'
import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
async function getNodeOutputImageCount(
comfyPage: ComfyPage,
nodeId: string
): Promise<number> {
return await comfyPage.page.evaluate(
(id) => window.app!.nodeOutputs?.[id]?.images?.length ?? 0,
nodeId
)
}
async function getWidgetValueSnapshot(
comfyPage: ComfyPage
): Promise<Record<string, Array<{ name: string; value: unknown }>>> {
return await comfyPage.page.evaluate(() => {
const nodes = window.app!.graph.nodes
const results: Record<string, Array<{ name: string; value: unknown }>> = {}
for (const node of nodes) {
if (node.widgets && node.widgets.length > 0) {
results[node.id] = node.widgets.map((w) => ({
name: w.name,
value: w.value
}))
}
}
return results
})
}
async function getLinkCount(comfyPage: ComfyPage): Promise<number> {
return await comfyPage.page.evaluate(() => {
return window.app!.graph.links
? Object.keys(window.app!.graph.links).length
: 0
})
}
test.describe('Workflow Persistence', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
@@ -71,7 +108,7 @@ test.describe('Workflow Persistence', () => {
const firstNode = await comfyPage.nodeOps.getFirstNodeRef()
expect(firstNode).toBeTruthy()
const nodeId = firstNode!.id
const nodeId = String(firstNode!.id)
// Simulate node outputs as if execution completed
await comfyPage.page.evaluate((id) => {
@@ -91,24 +128,20 @@ test.describe('Workflow Persistence', () => {
>
em.workflow?.activeWorkflow?.changeTracker?.checkState()
})
await comfyPage.nextFrame()
const outputsBefore = await comfyPage.page.evaluate((id) => {
return window.app!.nodeOutputs?.[id]
}, String(nodeId))
expect(outputsBefore).toBeTruthy()
await expect.poll(() => getNodeOutputImageCount(comfyPage, nodeId)).toBe(1)
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.nextFrame()
await comfyPage.workflow.waitForWorkflowIdle()
await tab.switchToWorkflow('outputs-test')
await comfyPage.nextFrame()
await comfyPage.workflow.waitForWorkflowIdle()
const outputsAfter = await comfyPage.page.evaluate((id) => {
return window.app!.nodeOutputs?.[id]
}, String(nodeId))
expect(outputsAfter).toBeTruthy()
expect(outputsAfter?.images).toBeDefined()
await expect
.poll(() => getNodeOutputImageCount(comfyPage, nodeId), {
timeout: 5_000
})
.toBe(1)
})
test('Loading a new workflow cleanly replaces the previous graph', async ({
@@ -149,43 +182,21 @@ test.describe('Workflow Persistence', () => {
// Read widget values via page.evaluate — these are internal LiteGraph
// state not exposed through DOM
const widgetValuesBefore = await comfyPage.page.evaluate(() => {
const nodes = window.app!.graph.nodes
const results: Record<string, unknown[]> = {}
for (const node of nodes) {
if (node.widgets && node.widgets.length > 0) {
results[node.id] = node.widgets.map((w) => ({
name: w.name,
value: w.value
}))
}
}
return results
})
const widgetValuesBefore = await getWidgetValueSnapshot(comfyPage)
expect(Object.keys(widgetValuesBefore).length).toBeGreaterThan(0)
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.nextFrame()
await comfyPage.workflow.waitForWorkflowIdle()
await tab.switchToWorkflow('widget-state-test')
await comfyPage.nextFrame()
await comfyPage.workflow.waitForWorkflowIdle()
const widgetValuesAfter = await comfyPage.page.evaluate(() => {
const nodes = window.app!.graph.nodes
const results: Record<string, unknown[]> = {}
for (const node of nodes) {
if (node.widgets && node.widgets.length > 0) {
results[node.id] = node.widgets.map((w) => ({
name: w.name,
value: w.value
}))
}
}
return results
})
expect(widgetValuesAfter).toEqual(widgetValuesBefore)
await expect
.poll(() => getWidgetValueSnapshot(comfyPage), {
timeout: 5_000
})
.toEqual(widgetValuesBefore)
})
test('API format workflow with missing node types partially loads', async ({
@@ -234,10 +245,10 @@ test.describe('Workflow Persistence', () => {
button: 'middle',
position: { x: 100, y: 100 }
})
await comfyPage.nextFrame()
const nodeCountAfter = await comfyPage.nodeOps.getNodeCount()
expect(nodeCountAfter).toBe(initialNodeCount)
await expect
.poll(() => comfyPage.nodeOps.getNodeCount())
.toBe(initialNodeCount)
})
test('Exported workflow does not contain transient blob: URLs', async ({
@@ -300,27 +311,20 @@ test.describe('Workflow Persistence', () => {
await tab.open()
// Link count requires internal graph state — not exposed via DOM
const linkCountBefore = await comfyPage.page.evaluate(() => {
return window.app!.graph.links
? Object.keys(window.app!.graph.links).length
: 0
})
const linkCountBefore = await getLinkCount(comfyPage)
expect(linkCountBefore).toBeGreaterThan(0)
await comfyPage.menu.topbar.saveWorkflow('links-test')
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.nextFrame()
await comfyPage.workflow.waitForWorkflowIdle()
await tab.switchToWorkflow('links-test')
await comfyPage.workflow.waitForWorkflowIdle()
const linkCountAfter = await comfyPage.page.evaluate(() => {
return window.app!.graph.links
? Object.keys(window.app!.graph.links).length
: 0
})
expect(linkCountAfter).toBe(linkCountBefore)
await expect
.poll(() => getLinkCount(comfyPage), { timeout: 5_000 })
.toBe(linkCountBefore)
})
test('Closing an unmodified inactive tab preserves both workflows', async ({

View File

@@ -93,9 +93,7 @@ test.describe('Zoom Controls', { tag: '@canvas' }, () => {
const input = comfyPage.page
.getByTestId(TestIds.canvas.zoomPercentageInput)
.locator('input')
await input.focus()
await comfyPage.page.keyboard.press('Control+a')
await input.pressSequentially('100')
await input.fill('100')
await input.press('Enter')
await comfyPage.nextFrame()