mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary Alright, alright, alright. These e2e tests have been runnin' around like they're late for somethin', settin' tight little timeouts like the world's gonna end in 250 milliseconds. Man, you gotta *breathe*. Let the framework do its thing. Go slow to go fast, that's what I always say. ## Changes - **What**: Removed ~120 redundant timeout overrides from auto-retrying Playwright assertions (`toBeVisible`, `toBeHidden`, `toHaveCount`, `toBeEnabled`, `toHaveAttribute`, `toContainText`, `expect.poll`) where 5000ms is already the default. Also removed sub-5s timeouts (1s, 2s, 3s) that were just *begging* for flaky failures — like wearin' a belt and suspenders and also holdin' your pants up with both hands. Raised the absurdly short timeouts in `customMatchers.ts` (250ms `toPass` → 5000ms, 256ms poll → default). Kept `timeout: 5000` on `.toPass()` calls (defaults to 0), `.waitFor()`, `waitForRequest`, `waitForFunction`, intentionally-short timeouts inside retry loops, and conditional `.isVisible()/.catch()` checks — those fellas actually need the help. ## Review Focus Every remaining timeout in the diff is there for a *reason*. The ones on `.toPass()` stay because that API defaults to zero — it won't retry at all without one. The ones on `.waitFor()` and `waitForRequest` stay because those are locator actions, not auto-retrying assertions. The intentionally-short ones inside `toPass` retry loops (`interaction.spec.ts`) and the negative assertions (`actionbar.spec.ts` confirming no response arrives) — those are *supposed* to be tight. The short timeouts on regular assertions were actively *encouragin'* flaky failures. That's like settin' your alarm for 4 AM and then gettin' mad you're tired. Just... don't do that, man. Let things take the time they need. 38 files, net -115 lines. Less code, more chill. That's livin'. --------- Co-authored-by: Amp <amp@ampcode.com>
426 lines
14 KiB
TypeScript
426 lines
14 KiB
TypeScript
import { expect } from '@playwright/test'
|
|
|
|
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
|
import { DefaultGraphPositions } from '@e2e/fixtures/constants/defaultGraphPositions'
|
|
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
|
})
|
|
|
|
test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
|
|
test('Truncates text when resized', async ({ comfyPage }) => {
|
|
await comfyPage.nodeOps.resizeNode(
|
|
DefaultGraphPositions.loadCheckpoint.pos,
|
|
DefaultGraphPositions.loadCheckpoint.size,
|
|
0.2,
|
|
1
|
|
)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'load-checkpoint-resized-min-width.png'
|
|
)
|
|
await comfyPage.closeMenu()
|
|
await comfyPage.nodeOps.resizeNode(
|
|
DefaultGraphPositions.ksampler.pos,
|
|
DefaultGraphPositions.ksampler.size,
|
|
0.2,
|
|
1
|
|
)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
`ksampler-resized-min-width.png`
|
|
)
|
|
})
|
|
|
|
test("Doesn't truncate when space still available", async ({ comfyPage }) => {
|
|
await comfyPage.nodeOps.resizeNode(
|
|
DefaultGraphPositions.emptyLatent.pos,
|
|
DefaultGraphPositions.emptyLatent.size,
|
|
0.8,
|
|
0.8
|
|
)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'empty-latent-resized-80-percent.png'
|
|
)
|
|
})
|
|
|
|
test('Can revert to full text', async ({ comfyPage }) => {
|
|
await comfyPage.nodeOps.resizeNode(
|
|
DefaultGraphPositions.loadCheckpoint.pos,
|
|
DefaultGraphPositions.loadCheckpoint.size,
|
|
0.8,
|
|
1,
|
|
true
|
|
)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('resized-to-original.png')
|
|
})
|
|
|
|
test('should refresh combo values of optional inputs', async ({
|
|
comfyPage
|
|
}) => {
|
|
const getComboValues = async () =>
|
|
comfyPage.page.evaluate(() => {
|
|
return window
|
|
.app!.graph!.nodes.find(
|
|
(node) => node.title === 'Node With Optional Combo Input'
|
|
)!
|
|
.widgets!.find((widget) => widget.name === 'optional_combo_input')!
|
|
.options.values
|
|
})
|
|
|
|
await comfyPage.workflow.loadWorkflow('inputs/optional_combo_input')
|
|
const initialComboValues = await getComboValues()
|
|
|
|
// Focus canvas
|
|
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 expect.poll(() => getComboValues()).not.toEqual(initialComboValues)
|
|
})
|
|
|
|
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')
|
|
|
|
await expect.poll(() => getComboValues()).toEqual(['A', 'B'])
|
|
})
|
|
})
|
|
|
|
test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => {
|
|
test('Can toggle', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/boolean_widget')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png')
|
|
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
|
|
const widget = await node.getWidget(0)
|
|
await widget.click()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'boolean_widget_toggled.png'
|
|
)
|
|
})
|
|
})
|
|
|
|
test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
|
|
test('Can drag adjust value', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('inputs/simple_slider')
|
|
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
|
|
}
|
|
})
|
|
await widget.dragHorizontal(50)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('slider_widget_dragged.png')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.page.evaluate(() => window.widgetValue))
|
|
.toBeDefined()
|
|
})
|
|
})
|
|
|
|
test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
|
|
test('Can drag adjust value', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/seed_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
|
|
}
|
|
})
|
|
await widget.dragHorizontal(50)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('seed_widget_dragged.png')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.page.evaluate(() => window.widgetValue))
|
|
.toBeDefined()
|
|
})
|
|
})
|
|
|
|
test.describe(
|
|
'Dynamic widget manipulation',
|
|
{ tag: ['@screenshot', '@widget'] },
|
|
() => {
|
|
test('Auto expand node when widget is added dynamically', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
|
|
const initialSize = await node.getSize()
|
|
|
|
await comfyPage.page.evaluate(() => {
|
|
window.app!.graph!.nodes[0].addWidget('number', 'new_widget', 10, null)
|
|
window.app!.graph!.setDirtyCanvas(true, true)
|
|
})
|
|
|
|
await expect
|
|
.poll(async () => (await node.getSize()).height)
|
|
.toBeGreaterThan(initialSize.height)
|
|
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'ksampler_widget_added.png'
|
|
)
|
|
})
|
|
}
|
|
)
|
|
|
|
test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
|
|
test('Can load image', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png', {
|
|
maxDiffPixels: 50
|
|
})
|
|
})
|
|
|
|
test('Can drag and drop image', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
|
|
|
// Get position of the load image node
|
|
const nodes = await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
|
|
const loadImageNode = nodes[0]
|
|
const { x, y } = await loadImageNode.getPosition()
|
|
|
|
// Drag and drop image file onto the load image node
|
|
await comfyPage.dragDrop.dragAndDropFile('image32x32.webp', {
|
|
dropPosition: { x, y }
|
|
})
|
|
|
|
// Expect the image preview to change automatically
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'image_preview_drag_and_dropped.png'
|
|
)
|
|
|
|
// Expect the filename combo value to be updated
|
|
const fileComboWidget = await loadImageNode.getWidget(0)
|
|
await expect.poll(() => fileComboWidget.getValue()).toBe('image32x32.webp')
|
|
})
|
|
|
|
test('Can change image by changing the filename combo value', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
|
const nodes = await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
|
|
const loadImageNode = nodes[0]
|
|
|
|
// Click the combo widget used to select the image filename
|
|
const fileComboWidget = await loadImageNode.getWidget(0)
|
|
await fileComboWidget.click()
|
|
|
|
// Select a new image filename value from the combo context menu
|
|
const comboEntry = comfyPage.page.getByRole('menuitem', {
|
|
name: 'image32x32.webp'
|
|
})
|
|
const imageLoaded = comfyPage.page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/view') &&
|
|
resp.url().includes('image32x32.webp') &&
|
|
resp.request().method() === 'GET' &&
|
|
resp.status() === 200
|
|
)
|
|
await comboEntry.click()
|
|
|
|
// Wait for the image to load from the server
|
|
await imageLoaded
|
|
|
|
// Wait for the image to decode and appear on the node
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
comfyPage.page.evaluate((nodeId) => {
|
|
const node = window.app!.graph!.getNodeById(nodeId)
|
|
const img = node?.imgs?.[0]
|
|
return (
|
|
!!img &&
|
|
img.complete &&
|
|
img.naturalWidth > 0 &&
|
|
img.src.includes('image32x32.webp')
|
|
)
|
|
}, loadImageNode.id),
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toBe(true)
|
|
await comfyPage.nextFrame()
|
|
|
|
// Expect the image preview to change automatically
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'image_preview_changed_by_combo_value.png',
|
|
{ maxDiffPixels: 50 }
|
|
)
|
|
|
|
// Expect the filename combo value to be updated
|
|
await expect.poll(() => fileComboWidget.getValue()).toBe('image32x32.webp')
|
|
})
|
|
test('Displays buttons when viewing single image of batch', async ({
|
|
comfyPage
|
|
}) => {
|
|
const [x, y] = await comfyPage.page.evaluate(() => {
|
|
const src =
|
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='768' height='512' viewBox='0 0 1 1'%3E%3Crect width='1' height='1' stroke='black'/%3E%3C/svg%3E"
|
|
const image1 = new Image()
|
|
image1.src = src
|
|
const image2 = new Image()
|
|
image2.src = src
|
|
const targetNode = graph!.nodes[6]
|
|
targetNode.imgs = [image1, image2]
|
|
targetNode.imageIndex = 1
|
|
app!.canvas.setDirty(true)
|
|
|
|
const x = targetNode.pos[0] + targetNode.size[0] - 41
|
|
const y = targetNode.pos[1] + targetNode.widgets!.at(-1)!.last_y! + 30
|
|
return app!.canvasPosToClientPos([x, y])
|
|
})
|
|
|
|
const clip = { x, y, width: 35, height: 35 }
|
|
await expect(comfyPage.page).toHaveScreenshot(
|
|
'image_preview_close_button.png',
|
|
{ clip }
|
|
)
|
|
})
|
|
})
|
|
|
|
test.describe(
|
|
'Animated image widget',
|
|
{ tag: ['@screenshot', '@widget'] },
|
|
() => {
|
|
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/load_animated_webp')
|
|
|
|
// Get position of the load animated webp node
|
|
const nodes = await comfyPage.nodeOps.getNodeRefsByType(
|
|
'DevToolsLoadAnimatedImageTest'
|
|
)
|
|
const loadAnimatedWebpNode = nodes[0]
|
|
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
|
|
|
// Drag and drop image file onto the load animated webp node
|
|
await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', {
|
|
dropPosition: { x, y },
|
|
waitForUpload: true
|
|
})
|
|
|
|
// Expect the filename combo value to be updated
|
|
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
|
|
await expect
|
|
.poll(() => fileComboWidget.getValue())
|
|
.toContain('animated_webp.webp')
|
|
})
|
|
|
|
test('Can preview saved animated webp image', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/save_animated_webp')
|
|
|
|
// Get position of the load animated webp node
|
|
const loadNodes = await comfyPage.nodeOps.getNodeRefsByType(
|
|
'DevToolsLoadAnimatedImageTest'
|
|
)
|
|
const loadAnimatedWebpNode = loadNodes[0]
|
|
const { x, y } = await loadAnimatedWebpNode.getPosition()
|
|
|
|
// Drag and drop image file onto the load animated webp node
|
|
await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', {
|
|
dropPosition: { x, y },
|
|
waitForUpload: true
|
|
})
|
|
|
|
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 =
|
|
await comfyPage.nodeOps.getNodeRefsByType('SaveAnimatedWEBP')
|
|
const saveAnimatedWebpNode = saveNodes[0]
|
|
if (!saveAnimatedWebpNode)
|
|
throw new Error('SaveAnimatedWEBP node not found')
|
|
|
|
// Simulate the graph executing
|
|
await comfyPage.page.evaluate(
|
|
([loadId, saveId]) => {
|
|
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
|
|
window.app!.nodeOutputs[saveId] = window.app!.nodeOutputs[loadId]
|
|
app!.canvas.setDirty(true)
|
|
},
|
|
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
|
|
)
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
comfyPage.page.evaluate(
|
|
([loadId, saveId]) => {
|
|
const graph = window.app!.graph
|
|
|
|
// Re-dirty the canvas so onDrawBackground fires again on the
|
|
// next frame. Without this, the single setDirty(true) above
|
|
// only triggers one paint; if the async image load inside
|
|
// showPreview() hasn't completed by then, node.imgs stays
|
|
// empty and no further paints re-check it.
|
|
window.app!.canvas.setDirty(true, true)
|
|
|
|
return [loadId, saveId].map(
|
|
(nodeId) => (graph.getNodeById(nodeId)?.imgs?.length ?? 0) > 0
|
|
)
|
|
},
|
|
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
|
|
),
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toEqual([true, true])
|
|
})
|
|
}
|
|
)
|
|
|
|
test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => {
|
|
test('Can load audio', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/load_audio_widget')
|
|
await expect(comfyPage.page.locator('.comfy-audio')).toBeVisible()
|
|
await expect(comfyPage.canvas).toHaveScreenshot('load_audio_widget.png')
|
|
})
|
|
})
|
|
|
|
test.describe('Unserialized widgets', { tag: '@widget' }, () => {
|
|
test('Unserialized widgets values do not mark graph as modified', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Add workflow w/ LoadImage node, which contains file upload and image preview widgets (not serialized)
|
|
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
|
|
|
// Move mouse and click to trigger the `graphEqual` check in `changeTracker.ts`
|
|
await comfyPage.page.mouse.move(10, 10)
|
|
await comfyPage.page.mouse.click(10, 10)
|
|
|
|
// Expect the graph to not be modified
|
|
await expect
|
|
.poll(() => comfyPage.workflow.isCurrentWorkflowModified())
|
|
.toBe(false)
|
|
})
|
|
})
|