mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-02 04:02:20 +00:00
Merge remote-tracking branch 'origin/main' into bl-more-slots
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Nodes Zoom', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,696 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { getMiddlePoint } from '../../../../fixtures/utils/litegraphUtils'
|
||||
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
||||
|
||||
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
||||
const box = await locator.boundingBox()
|
||||
if (!box) throw new Error('Slot bounding box not available')
|
||||
return {
|
||||
x: box.x + box.width / 2,
|
||||
y: box.y + box.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
async function getInputLinkDetails(
|
||||
page: Page,
|
||||
nodeId: NodeId,
|
||||
slotIndex: number
|
||||
) {
|
||||
return await page.evaluate(
|
||||
([targetNodeId, targetSlot]) => {
|
||||
const app = window['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) return null
|
||||
|
||||
const node = graph.getNodeById(targetNodeId)
|
||||
if (!node) return null
|
||||
|
||||
const input = node.inputs?.[targetSlot]
|
||||
if (!input) return null
|
||||
|
||||
const linkId = input.link
|
||||
if (linkId == null) return null
|
||||
|
||||
const link = graph.getLink?.(linkId)
|
||||
if (!link) return null
|
||||
|
||||
return {
|
||||
id: link.id,
|
||||
originId: link.origin_id,
|
||||
originSlot:
|
||||
typeof link.origin_slot === 'string'
|
||||
? Number.parseInt(link.origin_slot, 10)
|
||||
: link.origin_slot,
|
||||
targetId: link.target_id,
|
||||
targetSlot:
|
||||
typeof link.target_slot === 'string'
|
||||
? Number.parseInt(link.target_slot, 10)
|
||||
: link.target_slot,
|
||||
parentId: link.parentId ?? null
|
||||
}
|
||||
},
|
||||
[nodeId, slotIndex] as const
|
||||
)
|
||||
}
|
||||
|
||||
// Test helpers to reduce repetition across cases
|
||||
function slotLocator(
|
||||
page: Page,
|
||||
nodeId: NodeId,
|
||||
slotIndex: number,
|
||||
isInput: boolean
|
||||
) {
|
||||
const key = getSlotKey(String(nodeId), slotIndex, isInput)
|
||||
return page.locator(`[data-slot-key="${key}"]`)
|
||||
}
|
||||
|
||||
async function expectVisibleAll(...locators: Locator[]) {
|
||||
await Promise.all(locators.map((l) => expect(l).toBeVisible()))
|
||||
}
|
||||
|
||||
async function getSlotCenter(
|
||||
page: Page,
|
||||
nodeId: NodeId,
|
||||
slotIndex: number,
|
||||
isInput: boolean
|
||||
) {
|
||||
const locator = slotLocator(page, nodeId, slotIndex, isInput)
|
||||
await expect(locator).toBeVisible()
|
||||
return await getCenter(locator)
|
||||
}
|
||||
|
||||
async function connectSlots(
|
||||
page: Page,
|
||||
from: { nodeId: NodeId; index: number },
|
||||
to: { nodeId: NodeId; index: number },
|
||||
nextFrame: () => Promise<void>
|
||||
) {
|
||||
const fromLoc = slotLocator(page, from.nodeId, from.index, false)
|
||||
const toLoc = slotLocator(page, to.nodeId, to.index, true)
|
||||
await expectVisibleAll(fromLoc, toLoc)
|
||||
await fromLoc.dragTo(toLoc)
|
||||
await nextFrame()
|
||||
}
|
||||
|
||||
test.describe('Vue Node Link Interaction', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('vueNodes/simple-triple')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
await fitToViewInstant(comfyPage)
|
||||
})
|
||||
|
||||
test('should show a link dragging out from a slot when dragging on a slot', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(samplerNode).toBeTruthy()
|
||||
|
||||
const slot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
||||
await expect(slot).toBeVisible()
|
||||
|
||||
const start = await getCenter(slot)
|
||||
|
||||
// Arbitrary value
|
||||
const dragTarget = {
|
||||
x: start.x + 180,
|
||||
y: start.y - 140
|
||||
}
|
||||
|
||||
await comfyMouse.move(start)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
try {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-dragging-link.png'
|
||||
)
|
||||
} finally {
|
||||
await comfyMouse.drop()
|
||||
}
|
||||
})
|
||||
|
||||
test('should create a link when dropping on a compatible slot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: samplerNode.id, index: 0 },
|
||||
{ nodeId: vaeNode.id, index: 0 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(1)
|
||||
expect(await vaeInput.getLinkCount()).toBe(1)
|
||||
|
||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
||||
expect(linkDetails).not.toBeNull()
|
||||
expect(linkDetails).toMatchObject({
|
||||
originId: samplerNode.id,
|
||||
originSlot: 0,
|
||||
targetId: vaeNode.id,
|
||||
targetSlot: 0
|
||||
})
|
||||
})
|
||||
|
||||
test('should not create a link when slot types are incompatible', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
expect(samplerNode && clipNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const clipInput = await clipNode.getInput(0)
|
||||
|
||||
const outputSlot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
||||
const inputSlot = slotLocator(comfyPage.page, clipNode.id, 0, true)
|
||||
await expectVisibleAll(outputSlot, inputSlot)
|
||||
|
||||
await outputSlot.dragTo(inputSlot)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
expect(await clipInput.getLinkCount()).toBe(0)
|
||||
|
||||
const graphLinkDetails = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
clipNode.id,
|
||||
0
|
||||
)
|
||||
expect(graphLinkDetails).toBeNull()
|
||||
})
|
||||
|
||||
test('should not create a link when dropping onto a slot on the same node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(samplerNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const samplerInput = await samplerNode.getInput(3)
|
||||
|
||||
const outputSlot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
||||
const inputSlot = slotLocator(comfyPage.page, samplerNode.id, 3, true)
|
||||
await expectVisibleAll(outputSlot, inputSlot)
|
||||
|
||||
await outputSlot.dragTo(inputSlot)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
expect(await samplerInput.getLinkCount()).toBe(0)
|
||||
})
|
||||
|
||||
test('should reuse the existing origin when dragging an input link', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
const samplerOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const vaeInputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(samplerOutputCenter)
|
||||
await comfyMouse.drag(vaeInputCenter)
|
||||
await comfyMouse.drop()
|
||||
|
||||
const dragTarget = {
|
||||
x: vaeInputCenter.x + 160,
|
||||
y: vaeInputCenter.y - 100
|
||||
}
|
||||
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-input-drag-reuses-origin.png'
|
||||
)
|
||||
await comfyMouse.drop()
|
||||
})
|
||||
|
||||
test('ctrl+alt drag from an input starts a fresh link', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
const samplerOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const vaeInputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(samplerOutputCenter)
|
||||
await comfyMouse.drag(vaeInputCenter)
|
||||
await comfyMouse.drop()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const dragTarget = {
|
||||
x: vaeInputCenter.x + 140,
|
||||
y: vaeInputCenter.y - 110
|
||||
}
|
||||
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
|
||||
try {
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-input-drag-ctrl-alt.png'
|
||||
)
|
||||
} finally {
|
||||
await comfyMouse.drop().catch(() => {})
|
||||
await comfyPage.page.keyboard.up('Alt').catch(() => {})
|
||||
await comfyPage.page.keyboard.up('Control').catch(() => {})
|
||||
}
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Tcehnically intended to disconnect existing as well
|
||||
expect(await vaeInput.getLinkCount()).toBe(0)
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
})
|
||||
|
||||
test('dropping an input link back on its slot restores the original connection', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
const samplerOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const vaeInputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(samplerOutputCenter)
|
||||
try {
|
||||
await comfyMouse.drag(vaeInputCenter)
|
||||
} finally {
|
||||
await comfyMouse.drop()
|
||||
}
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const originalLink = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0
|
||||
)
|
||||
expect(originalLink).not.toBeNull()
|
||||
|
||||
const dragTarget = {
|
||||
x: vaeInputCenter.x + 150,
|
||||
y: vaeInputCenter.y - 100
|
||||
}
|
||||
|
||||
// To prevent needing a screenshot expectation for whether the link's off
|
||||
const vaeInputLocator = slotLocator(comfyPage.page, vaeNode.id, 0, true)
|
||||
const inputBox = await vaeInputLocator.boundingBox()
|
||||
if (!inputBox) throw new Error('Input slot bounding box not available')
|
||||
const isOutsideX =
|
||||
dragTarget.x < inputBox.x || dragTarget.x > inputBox.x + inputBox.width
|
||||
const isOutsideY =
|
||||
dragTarget.y < inputBox.y || dragTarget.y > inputBox.y + inputBox.height
|
||||
expect(isOutsideX || isOutsideY).toBe(true)
|
||||
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drop()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const restoredLink = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0
|
||||
)
|
||||
|
||||
expect(restoredLink).not.toBeNull()
|
||||
if (!restoredLink || !originalLink) {
|
||||
throw new Error('Expected both original and restored links to exist')
|
||||
}
|
||||
expect(restoredLink).toMatchObject({
|
||||
originId: originalLink.originId,
|
||||
originSlot: originalLink.originSlot,
|
||||
targetId: originalLink.targetId,
|
||||
targetSlot: originalLink.targetSlot,
|
||||
parentId: originalLink.parentId
|
||||
})
|
||||
expect(await samplerOutput.getLinkCount()).toBe(1)
|
||||
expect(await vaeInput.getLinkCount()).toBe(1)
|
||||
})
|
||||
|
||||
test('rerouted input drag preview remains anchored to reroute', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: samplerNode.id, index: 0 },
|
||||
{ nodeId: vaeNode.id, index: 0 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
const outputPosition = await samplerOutput.getPosition()
|
||||
const inputPosition = await vaeInput.getPosition()
|
||||
const reroutePoint = getMiddlePoint(outputPosition, inputPosition)
|
||||
|
||||
// Insert a reroute programmatically on the existing link between sampler output[0] and VAE input[0].
|
||||
// This avoids relying on an exact path hit-test position.
|
||||
await comfyPage.page.evaluate(
|
||||
([targetNodeId, targetSlot, clientPoint]) => {
|
||||
const app = (window as any)['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) throw new Error('Graph not available')
|
||||
const node = graph.getNodeById(targetNodeId)
|
||||
if (!node) throw new Error('Target node not found')
|
||||
const input = node.inputs?.[targetSlot]
|
||||
if (!input) throw new Error('Target input slot not found')
|
||||
|
||||
const linkId = input.link
|
||||
if (linkId == null) throw new Error('Expected existing link on input')
|
||||
const link = graph.getLink(linkId)
|
||||
if (!link) throw new Error('Link not found')
|
||||
|
||||
// Convert the client/canvas pixel coordinates to graph space
|
||||
const pos = app.canvas.ds.convertCanvasToOffset([
|
||||
clientPoint.x,
|
||||
clientPoint.y
|
||||
])
|
||||
graph.createReroute(pos, link)
|
||||
},
|
||||
[vaeNode.id, 0, reroutePoint] as const
|
||||
)
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const vaeInputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0,
|
||||
true
|
||||
)
|
||||
const dragTarget = {
|
||||
x: vaeInputCenter.x + 160,
|
||||
y: vaeInputCenter.y - 120
|
||||
}
|
||||
|
||||
let dropped = false
|
||||
try {
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-reroute-input-drag.png'
|
||||
)
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drop()
|
||||
dropped = true
|
||||
} finally {
|
||||
if (!dropped) {
|
||||
await comfyMouse.drop().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
||||
expect(linkDetails).not.toBeNull()
|
||||
expect(linkDetails?.originId).toBe(samplerNode.id)
|
||||
expect(linkDetails?.parentId).not.toBeNull()
|
||||
})
|
||||
|
||||
test('rerouted output shift-drag preview remains anchored to reroute', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: samplerNode.id, index: 0 },
|
||||
{ nodeId: vaeNode.id, index: 0 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
const outputPosition = await samplerOutput.getPosition()
|
||||
const inputPosition = await vaeInput.getPosition()
|
||||
const reroutePoint = getMiddlePoint(outputPosition, inputPosition)
|
||||
|
||||
// Insert a reroute programmatically on the existing link between sampler output[0] and VAE input[0].
|
||||
// This avoids relying on an exact path hit-test position.
|
||||
await comfyPage.page.evaluate(
|
||||
([targetNodeId, targetSlot, clientPoint]) => {
|
||||
const app = (window as any)['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) throw new Error('Graph not available')
|
||||
const node = graph.getNodeById(targetNodeId)
|
||||
if (!node) throw new Error('Target node not found')
|
||||
const input = node.inputs?.[targetSlot]
|
||||
if (!input) throw new Error('Target input slot not found')
|
||||
|
||||
const linkId = input.link
|
||||
if (linkId == null) throw new Error('Expected existing link on input')
|
||||
const link = graph.getLink(linkId)
|
||||
if (!link) throw new Error('Link not found')
|
||||
|
||||
// Convert the client/canvas pixel coordinates to graph space
|
||||
const pos = app.canvas.ds.convertCanvasToOffset([
|
||||
clientPoint.x,
|
||||
clientPoint.y
|
||||
])
|
||||
graph.createReroute(pos, link)
|
||||
},
|
||||
[vaeNode.id, 0, reroutePoint] as const
|
||||
)
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const outputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const dragTarget = {
|
||||
x: outputCenter.x + 150,
|
||||
y: outputCenter.y - 140
|
||||
}
|
||||
|
||||
let dropPending = false
|
||||
let shiftHeld = false
|
||||
try {
|
||||
await comfyMouse.move(outputCenter)
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
shiftHeld = true
|
||||
dropPending = true
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-reroute-output-shift-drag.png'
|
||||
)
|
||||
await comfyMouse.move(outputCenter)
|
||||
await comfyMouse.drop()
|
||||
dropPending = false
|
||||
} finally {
|
||||
if (dropPending) await comfyMouse.drop().catch(() => {})
|
||||
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
|
||||
}
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
||||
expect(linkDetails).not.toBeNull()
|
||||
expect(linkDetails?.originId).toBe(samplerNode.id)
|
||||
expect(linkDetails?.parentId).not.toBeNull()
|
||||
})
|
||||
|
||||
test('dragging input to input drags existing link', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(clipNode && samplerNode).toBeTruthy()
|
||||
|
||||
// Step 1: Connect CLIP's only output (index 0) to KSampler's second input (index 1)
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: clipNode.id, index: 0 },
|
||||
{ nodeId: samplerNode.id, index: 1 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
// Verify initial link exists between CLIP -> KSampler input[1]
|
||||
const initialLink = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
1
|
||||
)
|
||||
expect(initialLink).not.toBeNull()
|
||||
expect(initialLink).toMatchObject({
|
||||
originId: clipNode.id,
|
||||
targetId: samplerNode.id,
|
||||
targetSlot: 1
|
||||
})
|
||||
|
||||
// Step 2: Drag from KSampler's second input to its third input (index 2)
|
||||
const input2Center = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
1,
|
||||
true
|
||||
)
|
||||
const input3Center = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
2,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(input2Center)
|
||||
await comfyMouse.drag(input3Center)
|
||||
await comfyMouse.drop()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Expect old link removed from input[1]
|
||||
const afterSecondInput = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
1
|
||||
)
|
||||
expect(afterSecondInput).toBeNull()
|
||||
|
||||
// Expect new link exists at input[2] from CLIP
|
||||
const afterThirdInput = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
2
|
||||
)
|
||||
expect(afterThirdInput).not.toBeNull()
|
||||
expect(afterThirdInput).toMatchObject({
|
||||
originId: clipNode.id,
|
||||
targetId: samplerNode.id,
|
||||
targetSlot: 2
|
||||
})
|
||||
})
|
||||
|
||||
test('shift-dragging an output with multiple links should drag all links', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(clipNode && samplerNode).toBeTruthy()
|
||||
|
||||
const clipOutput = await clipNode.getOutput(0)
|
||||
|
||||
// Connect output[0] -> inputs[1] and [2]
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: clipNode.id, index: 0 },
|
||||
{ nodeId: samplerNode.id, index: 1 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: clipNode.id, index: 0 },
|
||||
{ nodeId: samplerNode.id, index: 2 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
expect(await clipOutput.getLinkCount()).toBe(2)
|
||||
|
||||
const outputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
clipNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const dragTarget = {
|
||||
x: outputCenter.x + 40,
|
||||
y: outputCenter.y - 140
|
||||
}
|
||||
|
||||
let dropPending = false
|
||||
let shiftHeld = false
|
||||
try {
|
||||
await comfyMouse.move(outputCenter)
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
shiftHeld = true
|
||||
await comfyMouse.drag(dragTarget)
|
||||
dropPending = true
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-shift-output-multi-link.png'
|
||||
)
|
||||
} finally {
|
||||
if (dropPending) await comfyMouse.drop().catch(() => {})
|
||||
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
|
||||
}
|
||||
})
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
145
browser_tests/tests/vueNodes/interactions/node/remove.spec.ts
Normal file
145
browser_tests/tests/vueNodes/interactions/node/remove.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Vue Nodes - Delete Key Interaction', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Enable Vue nodes rendering
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('Can select all and delete Vue nodes with Delete key', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
// Get initial Vue node count
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(initialNodeCount).toBeGreaterThan(0)
|
||||
|
||||
// Select all Vue nodes
|
||||
await comfyPage.ctrlA()
|
||||
|
||||
// Verify all Vue nodes are selected
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(initialNodeCount)
|
||||
|
||||
// Delete with Delete key
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
|
||||
// Verify all Vue nodes were deleted
|
||||
const finalNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(finalNodeCount).toBe(0)
|
||||
})
|
||||
|
||||
test('Can select specific Vue node and delete it', async ({ comfyPage }) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
// Get initial Vue node count
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(initialNodeCount).toBeGreaterThan(0)
|
||||
|
||||
// Get first Vue node ID and select it
|
||||
const nodeIds = await comfyPage.vueNodes.getNodeIds()
|
||||
await comfyPage.vueNodes.selectNode(nodeIds[0])
|
||||
|
||||
// Verify selection
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(1)
|
||||
|
||||
// Delete with Delete key
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
|
||||
// Verify one Vue node was deleted
|
||||
const finalNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(finalNodeCount).toBe(initialNodeCount - 1)
|
||||
})
|
||||
|
||||
test('Can select and delete Vue node with Backspace key', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
|
||||
// Select first Vue node
|
||||
const nodeIds = await comfyPage.vueNodes.getNodeIds()
|
||||
await comfyPage.vueNodes.selectNode(nodeIds[0])
|
||||
|
||||
// Delete with Backspace key instead of Delete
|
||||
await comfyPage.vueNodes.deleteSelectedWithBackspace()
|
||||
|
||||
// Verify Vue node was deleted
|
||||
const finalNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(finalNodeCount).toBe(initialNodeCount - 1)
|
||||
})
|
||||
|
||||
test('Delete key does not delete node when typing in Vue node widgets', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialNodeCount = await comfyPage.getGraphNodesCount()
|
||||
|
||||
// Find a text input widget in a Vue node
|
||||
const textWidget = comfyPage.page
|
||||
.locator('input[type="text"], textarea')
|
||||
.first()
|
||||
|
||||
// Click on text widget to focus it
|
||||
await textWidget.click()
|
||||
await textWidget.fill('test text')
|
||||
|
||||
// Press Delete while focused on widget - should delete text, not node
|
||||
await textWidget.press('Delete')
|
||||
|
||||
// Node count should remain the same
|
||||
const finalNodeCount = await comfyPage.getGraphNodesCount()
|
||||
expect(finalNodeCount).toBe(initialNodeCount)
|
||||
})
|
||||
|
||||
test('Delete key does not delete node when nothing is selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
// Ensure no Vue nodes are selected
|
||||
await comfyPage.vueNodes.clearSelection()
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(0)
|
||||
|
||||
// Press Delete key - should not crash and should handle gracefully
|
||||
await comfyPage.page.keyboard.press('Delete')
|
||||
|
||||
// Vue node count should remain the same
|
||||
const nodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(nodeCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Can multi-select with Ctrl+click and delete multiple Vue nodes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
|
||||
// Multi-select first two Vue nodes using Ctrl+click
|
||||
const nodeIds = await comfyPage.vueNodes.getNodeIds()
|
||||
const nodesToSelect = nodeIds.slice(0, 2)
|
||||
await comfyPage.vueNodes.selectNodes(nodesToSelect)
|
||||
|
||||
// Verify expected nodes are selected
|
||||
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
|
||||
expect(selectedCount).toBe(nodesToSelect.length)
|
||||
|
||||
// Delete selected Vue nodes
|
||||
await comfyPage.vueNodes.deleteSelected()
|
||||
|
||||
// Verify expected nodes were deleted
|
||||
const finalNodeCount = await comfyPage.vueNodes.getNodeCount()
|
||||
expect(finalNodeCount).toBe(initialNodeCount - nodesToSelect.length)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures'
|
||||
|
||||
test.describe('Vue Nodes Renaming', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('should display node title', async ({ comfyPage }) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(nodes.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
const title = await vueNode.getTitle()
|
||||
expect(title).toBe('KSampler')
|
||||
|
||||
// Verify title is visible in the header
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('KSampler')
|
||||
})
|
||||
|
||||
test('should allow title renaming by double clicking on the node header', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
// Test renaming with Enter
|
||||
await vueNode.setTitle('My Custom Sampler')
|
||||
const newTitle = await vueNode.getTitle()
|
||||
expect(newTitle).toBe('My Custom Sampler')
|
||||
|
||||
// Verify the title is displayed
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('My Custom Sampler')
|
||||
|
||||
// Test cancel with Escape
|
||||
const titleElement = await vueNode.getTitleElement()
|
||||
await titleElement.dblclick()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Type a different value but cancel
|
||||
const input = (await vueNode.getHeader()).locator(
|
||||
'[data-testid="node-title-input"]'
|
||||
)
|
||||
await input.fill('This Should Be Cancelled')
|
||||
await input.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Title should remain as the previously saved value
|
||||
const titleAfterCancel = await vueNode.getTitle()
|
||||
expect(titleAfterCancel).toBe('My Custom Sampler')
|
||||
})
|
||||
|
||||
test('Double click node body does not trigger edit', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const nodeBbox = await loadCheckpointNode.boundingBox()
|
||||
if (!nodeBbox) throw new Error('Node not found')
|
||||
await loadCheckpointNode.dblclick()
|
||||
|
||||
const editingTitleInput = comfyPage.page.getByTestId('node-title-input')
|
||||
await expect(editingTitleInput).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Vue Node Selection', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
const modifiers = [
|
||||
{ key: 'Control', name: 'ctrl' },
|
||||
{ key: 'Shift', name: 'shift' },
|
||||
{ key: 'Meta', name: 'meta' }
|
||||
] as const
|
||||
|
||||
for (const { key: modifier, name } of modifiers) {
|
||||
test(`should allow selecting multiple nodes with ${name}+click`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
|
||||
|
||||
await comfyPage.page.getByText('Empty Latent Image').click({
|
||||
modifiers: [modifier]
|
||||
})
|
||||
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
|
||||
|
||||
await comfyPage.page.getByText('KSampler').click({
|
||||
modifiers: [modifier]
|
||||
})
|
||||
expect(await 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 comfyPage.page.getByText('Load Checkpoint').click({
|
||||
modifiers: [modifier]
|
||||
})
|
||||
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user