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>
1401 lines
47 KiB
TypeScript
1401 lines
47 KiB
TypeScript
import type { Locator } from '@playwright/test'
|
|
import { expect } from '@playwright/test'
|
|
import type { Position } from '@vueuse/core'
|
|
|
|
import {
|
|
comfyPageFixture as test,
|
|
testComfySnapToGridGridSize
|
|
} from '@e2e/fixtures/ComfyPage'
|
|
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
|
import { DefaultGraphPositions } from '@e2e/fixtures/constants/defaultGraphPositions'
|
|
import { TestIds } from '@e2e/fixtures/selectors'
|
|
import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils'
|
|
import type { WorkspaceStore } from '@e2e/types/globals'
|
|
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
|
// Wait for the legacy menu to appear and canvas to settle after layout shift.
|
|
await comfyPage.page.locator('.comfy-menu').waitFor({ state: 'visible' })
|
|
await comfyPage.nextFrame()
|
|
})
|
|
|
|
test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
|
|
test('Can select/delete all items', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items')
|
|
await comfyPage.canvas.press('Control+a')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
|
|
await comfyPage.canvas.press('Delete')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('deleted-all.png')
|
|
})
|
|
|
|
test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items')
|
|
await comfyPage.canvas.press('Control+a')
|
|
await comfyPage.canvas.press('KeyP')
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot('pinned-all.png')
|
|
await comfyPage.canvas.press('KeyP')
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot('unpinned-all.png')
|
|
})
|
|
})
|
|
|
|
test.describe('Node Interaction', () => {
|
|
test('Can enter prompt', async ({ comfyPage }) => {
|
|
const textBox = comfyPage.widgetTextBox
|
|
await textBox.click()
|
|
await textBox.fill('Hello World')
|
|
await expect(textBox).toHaveValue('Hello World')
|
|
await textBox.fill('Hello World 2')
|
|
await expect(textBox).toHaveValue('Hello World 2')
|
|
})
|
|
|
|
test.describe('Node Selection', () => {
|
|
const multiSelectModifiers = ['Control', 'Shift', 'Meta'] as const
|
|
|
|
multiSelectModifiers.forEach((modifier) => {
|
|
test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
|
|
comfyPage
|
|
}) => {
|
|
const clipNodes =
|
|
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
|
for (const node of clipNodes) {
|
|
await node.click('title', { modifiers: [modifier] })
|
|
}
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
|
.toBe(clipNodes.length)
|
|
})
|
|
})
|
|
|
|
test(
|
|
'@2x Can highlight selected',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
|
await comfyPage.canvas.click({
|
|
position: DefaultGraphPositions.textEncodeNode1
|
|
})
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
|
|
await comfyPage.canvas.click({
|
|
position: DefaultGraphPositions.textEncodeNode2
|
|
})
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
|
|
}
|
|
)
|
|
|
|
const dragSelectNodes = async (
|
|
comfyPage: ComfyPage,
|
|
clipNodes: NodeReference[]
|
|
) => {
|
|
const clipNode1Pos = await clipNodes[0].getPosition()
|
|
const clipNode2Pos = await clipNodes[1].getPosition()
|
|
const offset = 64
|
|
await comfyPage.page.keyboard.down('Meta')
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
{
|
|
x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
|
|
y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset
|
|
},
|
|
{
|
|
x: Math.max(clipNode1Pos.x, clipNode2Pos.x) + offset,
|
|
y: Math.max(clipNode1Pos.y, clipNode2Pos.y) + offset
|
|
}
|
|
)
|
|
await comfyPage.page.keyboard.up('Meta')
|
|
}
|
|
|
|
test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => {
|
|
const clipNodes =
|
|
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
|
await dragSelectNodes(comfyPage, clipNodes)
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
|
.toBe(clipNodes.length)
|
|
})
|
|
|
|
test('Can move selected nodes using the Comfy.Canvas.MoveSelectedNodes.{Up|Down|Left|Right} commands', async ({
|
|
comfyPage
|
|
}) => {
|
|
const clipNodes =
|
|
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
|
const getPositions = () =>
|
|
Promise.all(clipNodes.map((node) => node.getPosition()))
|
|
const testDirection = async ({
|
|
direction,
|
|
expectedPosition
|
|
}: {
|
|
direction: string
|
|
expectedPosition: (originalPosition: Position) => Position
|
|
}) => {
|
|
const originalPositions = await getPositions()
|
|
await dragSelectNodes(comfyPage, clipNodes)
|
|
await comfyPage.command.executeCommand(
|
|
`Comfy.Canvas.MoveSelectedNodes.${direction}`
|
|
)
|
|
await comfyPage.canvas.press(`Control+Arrow${direction}`)
|
|
const newPositions = await getPositions()
|
|
expect(newPositions).toEqual(originalPositions.map(expectedPosition))
|
|
}
|
|
await testDirection({
|
|
direction: 'Down',
|
|
expectedPosition: (originalPosition) => ({
|
|
...originalPosition,
|
|
y: originalPosition.y + testComfySnapToGridGridSize
|
|
})
|
|
})
|
|
await testDirection({
|
|
direction: 'Right',
|
|
expectedPosition: (originalPosition) => ({
|
|
...originalPosition,
|
|
x: originalPosition.x + testComfySnapToGridGridSize
|
|
})
|
|
})
|
|
await testDirection({
|
|
direction: 'Up',
|
|
expectedPosition: (originalPosition) => ({
|
|
...originalPosition,
|
|
y: originalPosition.y - testComfySnapToGridGridSize
|
|
})
|
|
})
|
|
await testDirection({
|
|
direction: 'Left',
|
|
expectedPosition: (originalPosition) => ({
|
|
...originalPosition,
|
|
x: originalPosition.x - testComfySnapToGridGridSize
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
|
await comfyPage.nodeOps.dragTextEncodeNode2()
|
|
// Move mouse away to avoid hover highlight on the node at the drop position.
|
|
await comfyPage.canvasOps.moveMouseToEmptyArea()
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png', {
|
|
maxDiffPixels: 50
|
|
})
|
|
})
|
|
|
|
test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.LinkRelease.Action',
|
|
'no action'
|
|
)
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.LinkRelease.ActionShift',
|
|
'no action'
|
|
)
|
|
})
|
|
|
|
// Test both directions of edge connection.
|
|
;[{ reverse: false }, { reverse: true }].forEach(({ reverse }) => {
|
|
test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.canvasOps.disconnectEdge()
|
|
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
|
|
await comfyPage.canvasOps.connectEdge({ reverse })
|
|
// Move mouse to empty area to avoid slot highlight.
|
|
await comfyPage.canvasOps.moveMouseToEmptyArea()
|
|
// Litegraph renders edge with a slight offset.
|
|
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
|
|
maxDiffPixels: 50
|
|
})
|
|
})
|
|
})
|
|
|
|
test('Can move link', async ({ comfyPage }) => {
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
DefaultGraphPositions.clipTextEncodeNode1InputSlot,
|
|
DefaultGraphPositions.emptySpace
|
|
)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
DefaultGraphPositions.clipTextEncodeNode2InputSlot,
|
|
DefaultGraphPositions.clipTextEncodeNode1InputSlot
|
|
)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png')
|
|
})
|
|
|
|
test('Can copy link by shift-drag existing link', async ({ comfyPage }) => {
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
DefaultGraphPositions.clipTextEncodeNode1InputSlot,
|
|
DefaultGraphPositions.emptySpace
|
|
)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
|
|
await comfyPage.page.keyboard.down('Shift')
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
DefaultGraphPositions.clipTextEncodeNode2InputLinkPath,
|
|
DefaultGraphPositions.clipTextEncodeNode1InputSlot
|
|
)
|
|
await comfyPage.page.keyboard.up('Shift')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
|
|
})
|
|
|
|
test('Auto snap&highlight when dragging link over node', async ({
|
|
comfyPage,
|
|
comfyMouse
|
|
}) => {
|
|
await comfyPage.settings.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
|
|
await comfyPage.settings.setSetting('Comfy.Node.SnapHighlightsNode', true)
|
|
await comfyPage.nextFrame()
|
|
|
|
await comfyMouse.move(DefaultGraphPositions.clipTextEncodeNode1InputSlot)
|
|
await comfyMouse.drag(DefaultGraphPositions.clipTextEncodeNode2InputSlot)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png')
|
|
})
|
|
})
|
|
|
|
test(
|
|
'Can adjust widget value',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
await comfyPage.nodeOps.adjustEmptyLatentWidth()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'adjusted-widget-value.png'
|
|
)
|
|
}
|
|
)
|
|
|
|
test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('links/snap_to_slot')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
|
|
|
|
const outputSlotPos = {
|
|
x: 406,
|
|
y: 333
|
|
}
|
|
const samplerNodeCenterPos = {
|
|
x: 748,
|
|
y: 77
|
|
}
|
|
await comfyPage.canvasOps.dragAndDrop(outputSlotPos, samplerNodeCenterPos)
|
|
|
|
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
|
|
})
|
|
|
|
test(
|
|
'Can batch move links by drag with shift',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('links/batch_move_links')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
|
|
|
|
const outputSlot1Pos = {
|
|
x: 304,
|
|
y: 127
|
|
}
|
|
const outputSlot2Pos = {
|
|
x: 307,
|
|
y: 310
|
|
}
|
|
|
|
await comfyPage.page.keyboard.down('Shift')
|
|
await comfyPage.canvasOps.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
|
|
await comfyPage.page.keyboard.up('Shift')
|
|
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'batch_move_links_moved.png'
|
|
)
|
|
}
|
|
)
|
|
|
|
test(
|
|
'Can batch disconnect links with ctrl+alt+click',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
const loadCheckpointClipSlotPos = {
|
|
x: 332,
|
|
y: 508
|
|
}
|
|
await comfyPage.canvas.click({
|
|
modifiers: ['Control', 'Alt'],
|
|
position: loadCheckpointClipSlotPos
|
|
})
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'batch-disconnect-links-disconnected.png'
|
|
)
|
|
}
|
|
)
|
|
|
|
test(
|
|
'Can toggle dom widget node open/closed',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
// Find the node whose collapse toggler matches the hardcoded position.
|
|
// getNodeRefsByType order is non-deterministic, so identify by proximity.
|
|
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
|
const togglerPos = DefaultGraphPositions.textEncodeNodeToggler
|
|
let targetNode = nodes[0]
|
|
let minDist = Infinity
|
|
for (const n of nodes) {
|
|
const pos = await n.getPosition()
|
|
const dist = Math.hypot(pos.x - togglerPos.x, pos.y - togglerPos.y)
|
|
if (dist < minDist) {
|
|
minDist = dist
|
|
targetNode = n
|
|
}
|
|
}
|
|
|
|
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
|
|
await comfyPage.canvas.click({
|
|
position: togglerPos
|
|
})
|
|
await expect.poll(() => targetNode.isCollapsed()).toBe(true)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'text-encode-toggled-off.png'
|
|
)
|
|
// Wait for the double-click window (300ms) to expire so the next
|
|
// click at the same position isn't interpreted as a double-click.
|
|
await expect
|
|
.poll(() =>
|
|
comfyPage.page.evaluate(() => {
|
|
const pointer = window.app!.canvas.pointer
|
|
if (!pointer.eLastDown) return true
|
|
return performance.now() - pointer.eLastDown.timeStamp > 300
|
|
})
|
|
)
|
|
.toBe(true)
|
|
await comfyPage.canvas.click({
|
|
position: togglerPos
|
|
})
|
|
await expect.poll(() => targetNode.isCollapsed()).toBe(false)
|
|
// Move mouse away to avoid hover highlight differences.
|
|
await comfyPage.canvasOps.moveMouseToEmptyArea()
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'text-encode-toggled-back-open.png'
|
|
)
|
|
}
|
|
)
|
|
|
|
test('Can close prompt dialog with canvas click (number widget)', async ({
|
|
comfyPage
|
|
}) => {
|
|
const numberWidgetPos = {
|
|
x: 724,
|
|
y: 645
|
|
}
|
|
await comfyPage.canvas.click({
|
|
position: numberWidgetPos
|
|
})
|
|
const legacyPrompt = comfyPage.page.locator('.graphdialog')
|
|
await expect(legacyPrompt).toBeVisible()
|
|
// LiteGraph's graphdialog has a 256ms dismiss guard (Date.now() - clickTime > 256).
|
|
// Retry the canvas click until the dialog actually closes.
|
|
await expect(async () => {
|
|
await comfyPage.canvas.click({
|
|
position: {
|
|
x: 10,
|
|
y: 10
|
|
}
|
|
})
|
|
await expect(legacyPrompt).toBeHidden({ timeout: 500 })
|
|
}).toPass({ timeout: 5000 })
|
|
})
|
|
|
|
test('Can close prompt dialog with canvas click (text widget)', async ({
|
|
comfyPage
|
|
}) => {
|
|
const textWidgetPos = {
|
|
x: 167,
|
|
y: 143
|
|
}
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_save_image_node')
|
|
await comfyPage.canvas.click({
|
|
position: textWidgetPos
|
|
})
|
|
const legacyPrompt = comfyPage.page.locator('.graphdialog')
|
|
await expect(legacyPrompt).toBeVisible()
|
|
// LiteGraph's graphdialog has a 256ms dismiss guard (Date.now() - clickTime > 256).
|
|
// Retry the canvas click until the dialog actually closes.
|
|
await expect(async () => {
|
|
await comfyPage.canvas.click({
|
|
position: {
|
|
x: 10,
|
|
y: 10
|
|
}
|
|
})
|
|
await expect(legacyPrompt).toBeHidden({ timeout: 500 })
|
|
}).toPass({ timeout: 5000 })
|
|
})
|
|
|
|
test(
|
|
'Can double click node title to edit',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
await comfyPage.canvas.dblclick({
|
|
position: {
|
|
x: 50,
|
|
y: 10
|
|
},
|
|
delay: 5
|
|
})
|
|
await comfyPage.page.keyboard.type('Hello World')
|
|
await comfyPage.page.keyboard.press('Enter')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
|
|
}
|
|
)
|
|
|
|
test('Double click node body does not trigger edit', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
await comfyPage.canvas.dblclick({
|
|
position: {
|
|
x: 50,
|
|
y: 50
|
|
},
|
|
delay: 5
|
|
})
|
|
await expect(comfyPage.page.locator('.node-title-editor')).toHaveCount(0)
|
|
})
|
|
|
|
test(
|
|
'Can group selected nodes',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.GroupSelectedNodes.Padding',
|
|
10
|
|
)
|
|
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
|
|
await comfyPage.page.keyboard.down('Control')
|
|
await comfyPage.page.keyboard.press('KeyG')
|
|
await comfyPage.page.keyboard.up('Control')
|
|
await comfyPage.nextFrame()
|
|
// Confirm group title
|
|
await comfyPage.page.keyboard.press('Enter')
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'group-selected-nodes.png'
|
|
)
|
|
}
|
|
)
|
|
|
|
test(
|
|
'Can fit group to contents',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('groups/oversized_group')
|
|
await expect
|
|
.poll(() =>
|
|
comfyPage.page.evaluate(() => {
|
|
const group = window.app!.graph.groups[0]
|
|
return group ? [group.size[0], group.size[1]] : null
|
|
})
|
|
)
|
|
.not.toBeNull()
|
|
|
|
const initialGroupSize = await comfyPage.page.evaluate(() => {
|
|
const group = window.app!.graph.groups[0]
|
|
return group ? [group.size[0], group.size[1]] : null
|
|
})
|
|
|
|
await comfyPage.keyboard.selectAll()
|
|
await comfyPage.nextFrame()
|
|
await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents')
|
|
await expect
|
|
.poll(() =>
|
|
comfyPage.page.evaluate(() => {
|
|
const group = window.app!.graph.groups[0]
|
|
return group ? [group.size[0], group.size[1]] : null
|
|
})
|
|
)
|
|
.not.toEqual(initialGroupSize)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'group-fit-to-contents.png'
|
|
)
|
|
}
|
|
)
|
|
|
|
test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
|
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
|
|
const nodeRef = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
|
|
)[0]
|
|
|
|
await comfyPage.command.executeCommand(
|
|
'Comfy.Canvas.ToggleSelectedNodes.Pin'
|
|
)
|
|
await expect.poll(() => nodeRef.isPinned()).toBe(true)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('nodes-pinned.png')
|
|
await comfyPage.command.executeCommand(
|
|
'Comfy.Canvas.ToggleSelectedNodes.Pin'
|
|
)
|
|
await expect.poll(() => nodeRef.isPinned()).toBe(false)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
|
|
})
|
|
|
|
test(
|
|
'Can bypass/unbypass nodes with keyboard shortcut',
|
|
{ tag: '@screenshot' },
|
|
async ({ comfyPage }) => {
|
|
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
|
|
const nodeRef = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
|
|
)[0]
|
|
|
|
await comfyPage.canvas.press('Control+b')
|
|
await expect.poll(() => nodeRef.isBypassed()).toBe(true)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
|
|
await comfyPage.canvas.press('Control+b')
|
|
await expect.poll(() => nodeRef.isBypassed()).toBe(false)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png')
|
|
}
|
|
)
|
|
})
|
|
|
|
test.describe('Group Interaction', { tag: '@screenshot' }, () => {
|
|
test('Can double click group title to edit', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('groups/single_group')
|
|
await comfyPage.canvas.dblclick({
|
|
position: {
|
|
x: 50,
|
|
y: 10
|
|
},
|
|
delay: 5
|
|
})
|
|
await comfyPage.page.keyboard.type('Hello World')
|
|
await comfyPage.page.keyboard.press('Enter')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('group-title-edited.png')
|
|
})
|
|
})
|
|
|
|
test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
|
|
test('Can zoom in/out', async ({ comfyPage }) => {
|
|
await comfyPage.canvasOps.zoom(-100)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
|
|
await comfyPage.canvasOps.zoom(200)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png')
|
|
})
|
|
|
|
test('Can zoom very far out', async ({ comfyPage }) => {
|
|
await comfyPage.canvasOps.zoom(100, 12)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-very-far-out.png')
|
|
await comfyPage.canvasOps.zoom(-100, 12)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-back-in.png')
|
|
})
|
|
|
|
test('Can zoom in/out with ctrl+shift+vertical-drag', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.page.keyboard.down('Control')
|
|
await comfyPage.page.keyboard.down('Shift')
|
|
await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
|
|
await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out-ctrl-shift.png')
|
|
await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'zoomed-default-ctrl-shift.png'
|
|
)
|
|
await comfyPage.page.keyboard.up('Control')
|
|
await comfyPage.page.keyboard.up('Shift')
|
|
})
|
|
|
|
test('Can zoom in/out after decreasing canvas zoom speed setting', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.05)
|
|
await comfyPage.canvasOps.zoom(-100, 4)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'zoomed-in-low-zoom-speed.png'
|
|
)
|
|
await comfyPage.canvasOps.zoom(100, 8)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'zoomed-out-low-zoom-speed.png'
|
|
)
|
|
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
|
|
})
|
|
|
|
test('Can zoom in/out after increasing canvas zoom speed', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.5)
|
|
await comfyPage.canvasOps.zoom(-100, 4)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'zoomed-in-high-zoom-speed.png'
|
|
)
|
|
await comfyPage.canvasOps.zoom(100, 8)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'zoomed-out-high-zoom-speed.png'
|
|
)
|
|
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
|
|
})
|
|
|
|
test('Can pan', async ({ comfyPage }) => {
|
|
await comfyPage.canvasOps.pan({ x: 200, y: 200 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('panned.png')
|
|
})
|
|
|
|
test('Cursor style changes when panning', async ({ comfyPage }) => {
|
|
const getCursorStyle = async () => {
|
|
return await comfyPage.page.evaluate(() => {
|
|
return (
|
|
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
|
)
|
|
})
|
|
}
|
|
|
|
await comfyPage.page.mouse.move(10, 10)
|
|
await expect.poll(() => getCursorStyle()).toBe('default')
|
|
await comfyPage.page.mouse.down()
|
|
await expect.poll(() => getCursorStyle()).toBe('grabbing')
|
|
// Move mouse should not alter cursor style.
|
|
await comfyPage.page.mouse.move(10, 20)
|
|
await expect.poll(() => getCursorStyle()).toBe('grabbing')
|
|
await comfyPage.page.mouse.up()
|
|
await expect.poll(() => getCursorStyle()).toBe('default')
|
|
|
|
await comfyPage.page.keyboard.down('Space')
|
|
await expect.poll(() => getCursorStyle()).toBe('grab')
|
|
await comfyPage.page.mouse.down()
|
|
await expect.poll(() => getCursorStyle()).toBe('grabbing')
|
|
await comfyPage.page.mouse.up()
|
|
await expect.poll(() => getCursorStyle()).toBe('grab')
|
|
await comfyPage.page.keyboard.up('Space')
|
|
await expect.poll(() => getCursorStyle()).toBe('default')
|
|
})
|
|
|
|
// https://github.com/Comfy-Org/litegraph.js/pull/424
|
|
test('Properly resets dragging state after pan mode sequence', async ({
|
|
comfyPage
|
|
}) => {
|
|
const getCursorStyle = async () => {
|
|
return await comfyPage.page.evaluate(() => {
|
|
return (
|
|
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
|
)
|
|
})
|
|
}
|
|
|
|
// Initial state check
|
|
await comfyPage.page.mouse.move(10, 10)
|
|
await expect.poll(() => getCursorStyle()).toBe('default')
|
|
|
|
// Click and hold
|
|
await comfyPage.page.mouse.down()
|
|
await expect.poll(() => getCursorStyle()).toBe('grabbing')
|
|
|
|
// Press space while holding click
|
|
await comfyPage.page.keyboard.down('Space')
|
|
await expect.poll(() => getCursorStyle()).toBe('grabbing')
|
|
|
|
// Release click while space is still down
|
|
await comfyPage.page.mouse.up()
|
|
await expect.poll(() => getCursorStyle()).toBe('grab')
|
|
|
|
// Release space
|
|
await comfyPage.page.keyboard.up('Space')
|
|
await expect.poll(() => getCursorStyle()).toBe('default')
|
|
|
|
// Move mouse - cursor should remain default
|
|
await comfyPage.page.mouse.move(20, 20)
|
|
await expect.poll(() => getCursorStyle()).toBe('default')
|
|
})
|
|
|
|
test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => {
|
|
const posSlot1 = DefaultGraphPositions.clipTextEncodeNode1InputSlot
|
|
await comfyMouse.move(posSlot1)
|
|
const posEmpty = DefaultGraphPositions.emptySpace
|
|
await comfyMouse.drag(posEmpty)
|
|
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png')
|
|
|
|
await comfyPage.page.keyboard.down('Space')
|
|
await comfyMouse.mouse.move(posEmpty.x + 100, posEmpty.y + 100)
|
|
// Canvas should be panned.
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'panning-when-dragging-link.png'
|
|
)
|
|
await comfyPage.page.keyboard.up('Space')
|
|
await comfyMouse.move(posEmpty)
|
|
// Should be back to dragging link mode when space is released.
|
|
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link2.png')
|
|
await comfyMouse.drop()
|
|
})
|
|
|
|
test('Can pan very far and back', async ({ comfyPage }) => {
|
|
// intentionally slice the edge of where the clip text encode dom widgets are
|
|
await comfyPage.canvasOps.pan({ x: -800, y: -300 }, { x: 1000, y: 10 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('panned-step-one.png')
|
|
await comfyPage.canvasOps.pan({ x: -200, y: 0 }, { x: 1000, y: 10 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('panned-step-two.png')
|
|
await comfyPage.canvasOps.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('panned-far-away.png')
|
|
await comfyPage.canvasOps.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-from-far.png')
|
|
await comfyPage.canvasOps.pan({ x: 200, y: 0 }, { x: 1000, y: 10 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-two.png')
|
|
await comfyPage.canvasOps.pan({ x: 800, y: 300 }, { x: 1000, y: 10 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png')
|
|
})
|
|
|
|
test('@mobile Can pan with touch', async ({ comfyPage }) => {
|
|
await comfyPage.closeMenu()
|
|
await comfyPage.canvasOps.panWithTouch({ x: 200, y: 200 })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png')
|
|
})
|
|
})
|
|
|
|
test.describe('Widget Interaction', () => {
|
|
test('Undo text input', async ({ comfyPage }) => {
|
|
const textBox = comfyPage.widgetTextBox
|
|
await textBox.click()
|
|
await textBox.fill('')
|
|
await expect(textBox).toHaveValue('')
|
|
await textBox.fill('Hello World')
|
|
await expect(textBox).toHaveValue('Hello World')
|
|
await comfyPage.keyboard.undo(null)
|
|
await expect(textBox).toHaveValue('')
|
|
})
|
|
|
|
test('Undo attention edit', async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.EditAttention.Delta', 0.05)
|
|
const textBox = comfyPage.widgetTextBox
|
|
await textBox.click()
|
|
await textBox.fill('1girl')
|
|
await expect(textBox).toHaveValue('1girl')
|
|
await textBox.selectText()
|
|
await comfyPage.keyboard.moveUp(null)
|
|
await expect(textBox).toHaveValue('(1girl:1.05)')
|
|
await comfyPage.keyboard.undo(null)
|
|
await expect(textBox).toHaveValue('1girl')
|
|
})
|
|
})
|
|
|
|
test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
|
test('Can load workflow with string node id', async ({ comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow('nodes/string_node_id')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
|
|
})
|
|
|
|
test('Can load workflow with ("STRING",) input node', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('inputs/string_input')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
|
|
})
|
|
|
|
test('Creates initial workflow tab when persistence is disabled', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting('Comfy.Workflow.Persist', false)
|
|
await comfyPage.setup()
|
|
|
|
await expect
|
|
.poll(() =>
|
|
comfyPage.page.evaluate(() => {
|
|
return (window.app!.extensionManager as WorkspaceStore).workflow
|
|
.openWorkflows.length
|
|
})
|
|
)
|
|
.toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
test('Restore workflow on reload (switch workflow)', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
|
await comfyPage.setup({ clearStorage: false })
|
|
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
|
|
})
|
|
|
|
test('Restore workflow on reload (modify workflow)', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
|
|
await node.click('collapse')
|
|
await comfyPage.canvasOps.clickEmptySpace()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'single_ksampler_modified.png'
|
|
)
|
|
// Wait for V2 persistence debounce to save the modified workflow
|
|
const start = Date.now()
|
|
await comfyPage.page.waitForFunction((since) => {
|
|
for (let i = 0; i < window.localStorage.length; i++) {
|
|
const key = window.localStorage.key(i)
|
|
if (!key?.startsWith('Comfy.Workflow.DraftIndex.v2:')) continue
|
|
const json = window.localStorage.getItem(key)
|
|
if (!json) continue
|
|
try {
|
|
const index = JSON.parse(json)
|
|
if (typeof index.updatedAt === 'number' && index.updatedAt >= since) {
|
|
return true
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
return false
|
|
}, start)
|
|
await comfyPage.setup({ clearStorage: false })
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'single_ksampler_modified.png'
|
|
)
|
|
})
|
|
|
|
const generateUniqueFilename = (extension = '') =>
|
|
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
|
|
|
|
test.describe('Restore all open workflows on reload', () => {
|
|
let workflowA: string
|
|
let workflowB: string
|
|
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
|
|
|
workflowA = generateUniqueFilename()
|
|
await comfyPage.menu.topbar.saveWorkflow(workflowA)
|
|
workflowB = generateUniqueFilename()
|
|
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
|
await comfyPage.menu.topbar.saveWorkflow(workflowB)
|
|
|
|
// Wait for sessionStorage to persist the workflow paths before reloading
|
|
// V2 persistence uses sessionStorage with client-scoped keys
|
|
await comfyPage.page.waitForFunction(() => {
|
|
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
const key = window.sessionStorage.key(i)
|
|
if (key?.startsWith('Comfy.Workflow.OpenPaths:')) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
await comfyPage.setup({ clearStorage: false })
|
|
})
|
|
|
|
test('Restores topbar workflow tabs after reload', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Workflow.WorkflowTabsPosition',
|
|
'Topbar'
|
|
)
|
|
|
|
await expect
|
|
.poll(() => comfyPage.menu.topbar.getTabNames())
|
|
.toEqual(expect.arrayContaining([workflowA, workflowB]))
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const tabs = await comfyPage.menu.topbar.getTabNames()
|
|
return (
|
|
tabs.indexOf(workflowA) < tabs.indexOf(workflowB) &&
|
|
tabs.indexOf(workflowA) >= 0
|
|
)
|
|
})
|
|
.toBe(true)
|
|
|
|
await expect(comfyPage.menu.topbar.getActiveTab()).toContainText(
|
|
workflowB
|
|
)
|
|
})
|
|
|
|
test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Workflow.WorkflowTabsPosition',
|
|
'Sidebar'
|
|
)
|
|
await comfyPage.menu.workflowsTab.open()
|
|
await expect
|
|
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
|
.toEqual(expect.arrayContaining([workflowA, workflowB]))
|
|
await expect
|
|
.poll(async () => {
|
|
const ws = await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
|
return ws.indexOf(workflowA) < ws.indexOf(workflowB)
|
|
})
|
|
.toBe(true)
|
|
await expect(comfyPage.menu.workflowsTab.activeWorkflowLabel).toHaveText(
|
|
workflowB
|
|
)
|
|
})
|
|
})
|
|
|
|
test.describe('Restore workflow tabs after browser restart', () => {
|
|
let workflowA: string
|
|
let workflowB: string
|
|
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
|
|
|
workflowA = generateUniqueFilename()
|
|
await comfyPage.menu.topbar.saveWorkflow(workflowA)
|
|
workflowB = generateUniqueFilename()
|
|
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
|
await comfyPage.menu.topbar.saveWorkflow(workflowB)
|
|
|
|
// Wait for localStorage fallback pointers to be written
|
|
await comfyPage.page.waitForFunction(() => {
|
|
for (let i = 0; i < window.localStorage.length; i++) {
|
|
const key = window.localStorage.key(i)
|
|
if (key?.startsWith('Comfy.Workflow.LastOpenPaths:')) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
|
|
// Simulate browser restart: clear sessionStorage (lost on close)
|
|
// but keep localStorage (survives browser restart)
|
|
await comfyPage.page.evaluate(() => {
|
|
sessionStorage.clear()
|
|
})
|
|
await comfyPage.setup({ clearStorage: false })
|
|
})
|
|
|
|
test('Restores topbar workflow tabs after browser restart', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Workflow.WorkflowTabsPosition',
|
|
'Topbar'
|
|
)
|
|
// Wait for both restored tabs to render (localStorage fallback is async)
|
|
await expect(
|
|
comfyPage.page.locator('.workflow-tabs .workflow-label', {
|
|
hasText: workflowA
|
|
})
|
|
).toBeVisible()
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
const tabs = await comfyPage.menu.topbar.getTabNames()
|
|
return (
|
|
tabs.includes(workflowA) &&
|
|
tabs.includes(workflowB) &&
|
|
tabs.indexOf(workflowA) < tabs.indexOf(workflowB)
|
|
)
|
|
})
|
|
.toBe(true)
|
|
|
|
await expect(comfyPage.menu.topbar.getActiveTab()).toContainText(
|
|
workflowB
|
|
)
|
|
})
|
|
|
|
test('Restores sidebar workflows after browser restart', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Workflow.WorkflowTabsPosition',
|
|
'Sidebar'
|
|
)
|
|
await comfyPage.menu.workflowsTab.open()
|
|
await expect
|
|
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
|
.toEqual(expect.arrayContaining([workflowA, workflowB]))
|
|
await expect
|
|
.poll(async () => {
|
|
const ws = await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
|
return ws.indexOf(workflowA) < ws.indexOf(workflowB)
|
|
})
|
|
.toBe(true)
|
|
await expect(comfyPage.menu.workflowsTab.activeWorkflowLabel).toHaveText(
|
|
workflowB
|
|
)
|
|
})
|
|
})
|
|
|
|
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.EnableWorkflowViewRestore',
|
|
false
|
|
)
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
|
|
})
|
|
})
|
|
|
|
test.describe('Load duplicate workflow', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
|
})
|
|
|
|
test('A workflow can be loaded multiple times in a row', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
await comfyPage.menu.workflowsTab.open()
|
|
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
|
|
})
|
|
})
|
|
|
|
test.describe('Viewport settings', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Workflow.WorkflowTabsPosition',
|
|
'Topbar'
|
|
)
|
|
|
|
await comfyPage.workflow.setupWorkflowsDirectory({})
|
|
})
|
|
|
|
test('Keeps viewport settings when changing tabs', async ({
|
|
comfyPage,
|
|
comfyMouse
|
|
}) => {
|
|
const changeTab = async (tab: Locator) => {
|
|
await tab.click()
|
|
await comfyPage.nextFrame()
|
|
await comfyMouse.move(DefaultGraphPositions.emptySpace)
|
|
|
|
// If tooltip is visible, wait for it to hide
|
|
await expect(
|
|
comfyPage.page.locator('.workflow-popover-fade')
|
|
).toHaveCount(0)
|
|
}
|
|
|
|
// Screenshot the canvas element
|
|
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
|
|
|
|
const toggleButton = comfyPage.page.getByTestId(
|
|
TestIds.canvas.toggleMinimapButton
|
|
)
|
|
await toggleButton.click()
|
|
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false)
|
|
|
|
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
|
|
await comfyPage.nextFrame()
|
|
|
|
// Save workflow as a new file, then zoom out before screen shot
|
|
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
|
|
|
|
await comfyPage.nextFrame()
|
|
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
|
await changeTab(tabA)
|
|
|
|
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
|
|
|
|
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
|
await changeTab(tabB)
|
|
|
|
await comfyMouse.move(DefaultGraphPositions.emptySpace)
|
|
for (let i = 0; i < 4; i++) {
|
|
await comfyMouse.wheel(0, 60)
|
|
}
|
|
|
|
await comfyPage.nextFrame()
|
|
const screenshotB = (await comfyPage.canvas.screenshot()).toString('base64')
|
|
|
|
// Ensure that the screenshots are different due to zoom level
|
|
expect(screenshotB).not.toBe(screenshotA)
|
|
|
|
// Go back to Workflow A
|
|
await changeTab(tabA)
|
|
expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(
|
|
screenshotA
|
|
)
|
|
|
|
// And back to Workflow B
|
|
await changeTab(tabB)
|
|
expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(
|
|
screenshotB
|
|
)
|
|
})
|
|
})
|
|
|
|
test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
|
|
test.describe('Legacy Mode', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Canvas.NavigationMode',
|
|
'legacy'
|
|
)
|
|
})
|
|
|
|
test('Left-click drag in empty area should pan canvas', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
{ x: 50, y: 50 },
|
|
{ x: 150, y: 150 }
|
|
)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'legacy-left-drag-pan.png'
|
|
)
|
|
})
|
|
|
|
test('Middle-click drag should pan canvas', async ({ comfyPage }) => {
|
|
await comfyPage.page.mouse.move(50, 50)
|
|
await comfyPage.page.mouse.down({ button: 'middle' })
|
|
await comfyPage.page.mouse.move(150, 150)
|
|
await comfyPage.page.mouse.up({ button: 'middle' })
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'legacy-middle-drag-pan.png'
|
|
)
|
|
})
|
|
|
|
test('Mouse wheel should zoom in/out', async ({ comfyPage }) => {
|
|
await comfyPage.page.mouse.move(400, 300)
|
|
await comfyPage.page.mouse.wheel(0, -120)
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'legacy-wheel-zoom-in.png'
|
|
)
|
|
|
|
await comfyPage.page.mouse.wheel(0, 240)
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'legacy-wheel-zoom-out.png'
|
|
)
|
|
})
|
|
|
|
test('Left-click on node should not pan canvas', async ({ comfyPage }) => {
|
|
await comfyPage.canvas.click({
|
|
position: DefaultGraphPositions.textEncodeNode1
|
|
})
|
|
await comfyPage.nextFrame()
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
|
.toBe(1)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'legacy-click-node-select.png'
|
|
)
|
|
})
|
|
})
|
|
|
|
test.describe('Standard Mode', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Canvas.NavigationMode',
|
|
'standard'
|
|
)
|
|
})
|
|
|
|
test('Left-click drag in empty area should select nodes', async ({
|
|
comfyPage
|
|
}) => {
|
|
const clipNodes =
|
|
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
|
const clipNode1Pos = await clipNodes[0].getPosition()
|
|
const clipNode2Pos = await clipNodes[1].getPosition()
|
|
const offset = 64
|
|
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
{
|
|
x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
|
|
y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset
|
|
},
|
|
{
|
|
x: Math.max(clipNode1Pos.x, clipNode2Pos.x) + offset,
|
|
y: Math.max(clipNode1Pos.y, clipNode2Pos.y) + offset
|
|
}
|
|
)
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
|
.toBe(clipNodes.length)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-left-drag-select.png'
|
|
)
|
|
})
|
|
|
|
test('Middle-click drag should pan canvas', async ({ comfyPage }) => {
|
|
await comfyPage.page.mouse.move(50, 50)
|
|
await comfyPage.page.mouse.down({ button: 'middle' })
|
|
await comfyPage.page.mouse.move(150, 150)
|
|
await comfyPage.page.mouse.up({ button: 'middle' })
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-middle-drag-pan.png'
|
|
)
|
|
})
|
|
|
|
test('Ctrl + mouse wheel should zoom in/out', async ({ comfyPage }) => {
|
|
await comfyPage.page.mouse.move(400, 300)
|
|
await comfyPage.page.keyboard.down('Control')
|
|
await comfyPage.page.mouse.wheel(0, -120)
|
|
await comfyPage.page.keyboard.up('Control')
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-ctrl-wheel-zoom-in.png'
|
|
)
|
|
|
|
await comfyPage.page.keyboard.down('Control')
|
|
await comfyPage.page.mouse.wheel(0, 240)
|
|
await comfyPage.page.keyboard.up('Control')
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-ctrl-wheel-zoom-out.png'
|
|
)
|
|
})
|
|
|
|
test('Left-click on node should select node (not start selection box)', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.canvas.click({
|
|
position: DefaultGraphPositions.textEncodeNode1
|
|
})
|
|
await comfyPage.nextFrame()
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
|
.toBe(1)
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-click-node-select.png'
|
|
)
|
|
})
|
|
|
|
test('Space + left-click drag should pan canvas', async ({ comfyPage }) => {
|
|
// Click canvas to focus it
|
|
await comfyPage.page.click('canvas')
|
|
await comfyPage.nextFrame()
|
|
|
|
await comfyPage.page.keyboard.down('Space')
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
{ x: 50, y: 50 },
|
|
{ x: 150, y: 150 }
|
|
)
|
|
await comfyPage.page.keyboard.up('Space')
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-space-drag-pan.png'
|
|
)
|
|
})
|
|
|
|
test('Space key overrides default left-click behavior', async ({
|
|
comfyPage
|
|
}) => {
|
|
const clipNodes =
|
|
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
|
const clipNode1Pos = await clipNodes[0].getPosition()
|
|
const offset = 64
|
|
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
{
|
|
x: clipNode1Pos.x - offset,
|
|
y: clipNode1Pos.y - offset
|
|
},
|
|
{
|
|
x: clipNode1Pos.x + offset,
|
|
y: clipNode1Pos.y + offset
|
|
}
|
|
)
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
|
.toBeGreaterThan(0)
|
|
|
|
await comfyPage.canvasOps.clickEmptySpace()
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
|
.toBe(0)
|
|
|
|
await comfyPage.page.keyboard.down('Space')
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
{
|
|
x: clipNode1Pos.x - offset,
|
|
y: clipNode1Pos.y - offset
|
|
},
|
|
{
|
|
x: clipNode1Pos.x + offset,
|
|
y: clipNode1Pos.y + offset
|
|
}
|
|
)
|
|
await comfyPage.page.keyboard.up('Space')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
|
.toBe(0)
|
|
})
|
|
})
|
|
|
|
test('Shift + mouse wheel should pan canvas horizontally', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Canvas.MouseWheelScroll',
|
|
'panning'
|
|
)
|
|
|
|
await comfyPage.page.click('canvas')
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect(comfyPage.canvas).toHaveScreenshot('standard-initial.png')
|
|
|
|
await comfyPage.page.mouse.move(400, 300)
|
|
|
|
await comfyPage.page.keyboard.down('Shift')
|
|
await comfyPage.page.mouse.wheel(0, 120)
|
|
await comfyPage.page.keyboard.up('Shift')
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-shift-wheel-pan-right.png'
|
|
)
|
|
|
|
await comfyPage.page.keyboard.down('Shift')
|
|
await comfyPage.page.mouse.wheel(0, -240)
|
|
await comfyPage.page.keyboard.up('Shift')
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-shift-wheel-pan-left.png'
|
|
)
|
|
|
|
await comfyPage.page.keyboard.down('Shift')
|
|
await comfyPage.page.mouse.wheel(0, 120)
|
|
await comfyPage.page.keyboard.up('Shift')
|
|
await comfyPage.nextFrame()
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'standard-shift-wheel-pan-center.png'
|
|
)
|
|
})
|
|
|
|
test.describe('Edge Cases', () => {
|
|
test('Multiple modifier keys work correctly in legacy mode', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Canvas.NavigationMode',
|
|
'legacy'
|
|
)
|
|
|
|
await comfyPage.page.keyboard.down('Alt')
|
|
await comfyPage.page.keyboard.down('Shift')
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
{ x: 50, y: 50 },
|
|
{ x: 150, y: 150 }
|
|
)
|
|
await comfyPage.page.keyboard.up('Shift')
|
|
await comfyPage.page.keyboard.up('Alt')
|
|
|
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
|
'legacy-alt-shift-drag.png'
|
|
)
|
|
})
|
|
|
|
test('Cursor changes appropriately in different modes', async ({
|
|
comfyPage
|
|
}) => {
|
|
const getCursorStyle = async () => {
|
|
return await comfyPage.page.evaluate(() => {
|
|
return (
|
|
document.getElementById('graph-canvas')!.style.cursor || 'default'
|
|
)
|
|
})
|
|
}
|
|
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.Canvas.NavigationMode',
|
|
'legacy'
|
|
)
|
|
await comfyPage.page.mouse.move(50, 50)
|
|
await comfyPage.page.mouse.down()
|
|
await expect.poll(() => getCursorStyle()).toBe('grabbing')
|
|
await comfyPage.page.mouse.up()
|
|
})
|
|
})
|
|
})
|