mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-09 01:20:09 +00:00
811 lines
25 KiB
TypeScript
811 lines
25 KiB
TypeScript
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.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 samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
|
|
const samplerNode = samplerNodes[0]
|
|
const outputSlot = await samplerNode.getOutput(0)
|
|
await outputSlot.removeLinks()
|
|
await comfyPage.nextFrame()
|
|
|
|
const slotKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`)
|
|
await expect(slotLocator).toBeVisible()
|
|
|
|
const start = await getCenter(slotLocator)
|
|
const canvasBox = await comfyPage.canvas.boundingBox()
|
|
if (!canvasBox) throw new Error('Canvas bounding box not available')
|
|
|
|
// 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 samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
const samplerNode = samplerNodes[0]
|
|
|
|
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
|
|
expect(vaeNodes.length).toBeGreaterThan(0)
|
|
const vaeNode = vaeNodes[0]
|
|
|
|
const samplerOutput = await samplerNode.getOutput(0)
|
|
const vaeInput = await vaeNode.getInput(0)
|
|
|
|
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true)
|
|
|
|
const outputSlot = comfyPage.page.locator(
|
|
`[data-slot-key="${outputSlotKey}"]`
|
|
)
|
|
const inputSlot = comfyPage.page.locator(
|
|
`[data-slot-key="${inputSlotKey}"]`
|
|
)
|
|
|
|
await expect(outputSlot).toBeVisible()
|
|
await expect(inputSlot).toBeVisible()
|
|
|
|
await outputSlot.dragTo(inputSlot)
|
|
await comfyPage.nextFrame()
|
|
|
|
expect(await samplerOutput.getLinkCount()).toBe(1)
|
|
expect(await vaeInput.getLinkCount()).toBe(1)
|
|
|
|
const linkDetails = await comfyPage.page.evaluate((sourceId) => {
|
|
const app = window['app']
|
|
const graph = app?.canvas?.graph ?? app?.graph
|
|
if (!graph) return null
|
|
|
|
const source = graph.getNodeById(sourceId)
|
|
if (!source) return null
|
|
|
|
const linkId = source.outputs[0]?.links?.[0]
|
|
if (linkId == null) return null
|
|
|
|
const link = graph.links[linkId]
|
|
if (!link) return null
|
|
|
|
return {
|
|
originId: link.origin_id,
|
|
originSlot: link.origin_slot,
|
|
targetId: link.target_id,
|
|
targetSlot: link.target_slot
|
|
}
|
|
}, samplerNode.id)
|
|
|
|
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 samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
const samplerNode = samplerNodes[0]
|
|
|
|
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
|
expect(clipNodes.length).toBeGreaterThan(0)
|
|
const clipNode = clipNodes[0]
|
|
|
|
const samplerOutput = await samplerNode.getOutput(0)
|
|
const clipInput = await clipNode.getInput(0)
|
|
|
|
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const inputSlotKey = getSlotKey(String(clipNode.id), 0, true)
|
|
|
|
const outputSlot = comfyPage.page.locator(
|
|
`[data-slot-key="${outputSlotKey}"]`
|
|
)
|
|
const inputSlot = comfyPage.page.locator(
|
|
`[data-slot-key="${inputSlotKey}"]`
|
|
)
|
|
|
|
await expect(outputSlot).toBeVisible()
|
|
await expect(inputSlot).toBeVisible()
|
|
|
|
await outputSlot.dragTo(inputSlot)
|
|
await comfyPage.nextFrame()
|
|
|
|
expect(await samplerOutput.getLinkCount()).toBe(0)
|
|
expect(await clipInput.getLinkCount()).toBe(0)
|
|
|
|
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
|
|
const app = window['app']
|
|
const graph = app?.canvas?.graph ?? app?.graph
|
|
if (!graph) return 0
|
|
|
|
const source = graph.getNodeById(sourceId)
|
|
if (!source) return 0
|
|
|
|
return source.outputs[0]?.links?.length ?? 0
|
|
}, samplerNode.id)
|
|
|
|
expect(graphLinkCount).toBe(0)
|
|
})
|
|
|
|
test('should not create a link when dropping onto a slot on the same node', async ({
|
|
comfyPage
|
|
}) => {
|
|
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
const samplerNode = samplerNodes[0]
|
|
|
|
const samplerOutput = await samplerNode.getOutput(0)
|
|
const samplerInput = await samplerNode.getInput(3)
|
|
|
|
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true)
|
|
|
|
const outputSlot = comfyPage.page.locator(
|
|
`[data-slot-key="${outputSlotKey}"]`
|
|
)
|
|
const inputSlot = comfyPage.page.locator(
|
|
`[data-slot-key="${inputSlotKey}"]`
|
|
)
|
|
|
|
await expect(outputSlot).toBeVisible()
|
|
await expect(inputSlot).toBeVisible()
|
|
|
|
await outputSlot.dragTo(inputSlot)
|
|
await comfyPage.nextFrame()
|
|
|
|
expect(await samplerOutput.getLinkCount()).toBe(0)
|
|
expect(await samplerInput.getLinkCount()).toBe(0)
|
|
|
|
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
|
|
const app = window['app']
|
|
const graph = app?.canvas?.graph ?? app?.graph
|
|
if (!graph) return 0
|
|
|
|
const source = graph.getNodeById(sourceId)
|
|
if (!source) return 0
|
|
|
|
return source.outputs[0]?.links?.length ?? 0
|
|
}, samplerNode.id)
|
|
|
|
expect(graphLinkCount).toBe(0)
|
|
})
|
|
|
|
test('should reuse the existing origin when dragging an input link', async ({
|
|
comfyPage,
|
|
comfyMouse
|
|
}) => {
|
|
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
|
|
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
expect(vaeNodes.length).toBeGreaterThan(0)
|
|
|
|
const samplerNode = samplerNodes[0]
|
|
const vaeNode = vaeNodes[0]
|
|
|
|
const samplerOutputKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const vaeInputKey = getSlotKey(String(vaeNode.id), 0, true)
|
|
|
|
const samplerOutputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerOutputKey}"]`
|
|
)
|
|
const vaeInputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${vaeInputKey}"]`
|
|
)
|
|
|
|
await expect(samplerOutputLocator).toBeVisible()
|
|
await expect(vaeInputLocator).toBeVisible()
|
|
|
|
const samplerOutputCenter = await getCenter(samplerOutputLocator)
|
|
const vaeInputCenter = await getCenter(vaeInputLocator)
|
|
|
|
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 samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
|
|
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
expect(vaeNodes.length).toBeGreaterThan(0)
|
|
|
|
const samplerNode = samplerNodes[0]
|
|
const vaeNode = vaeNodes[0]
|
|
|
|
const samplerOutput = await samplerNode.getOutput(0)
|
|
const vaeInput = await vaeNode.getInput(0)
|
|
|
|
const samplerOutputKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const vaeInputKey = getSlotKey(String(vaeNode.id), 0, true)
|
|
|
|
const samplerOutputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerOutputKey}"]`
|
|
)
|
|
const vaeInputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${vaeInputKey}"]`
|
|
)
|
|
|
|
await expect(samplerOutputLocator).toBeVisible()
|
|
await expect(vaeInputLocator).toBeVisible()
|
|
|
|
const samplerOutputCenter = await getCenter(samplerOutputLocator)
|
|
const vaeInputCenter = await getCenter(vaeInputLocator)
|
|
|
|
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 samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
|
|
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
expect(vaeNodes.length).toBeGreaterThan(0)
|
|
|
|
const samplerNode = samplerNodes[0]
|
|
const vaeNode = vaeNodes[0]
|
|
|
|
const samplerOutput = await samplerNode.getOutput(0)
|
|
const vaeInput = await vaeNode.getInput(0)
|
|
|
|
const samplerOutputKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const vaeInputKey = getSlotKey(String(vaeNode.id), 0, true)
|
|
|
|
const samplerOutputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerOutputKey}"]`
|
|
)
|
|
const vaeInputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${vaeInputKey}"]`
|
|
)
|
|
|
|
await expect(samplerOutputLocator).toBeVisible()
|
|
await expect(vaeInputLocator).toBeVisible()
|
|
|
|
const samplerOutputCenter = await getCenter(samplerOutputLocator)
|
|
const vaeInputCenter = await getCenter(vaeInputLocator)
|
|
|
|
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 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 samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
|
|
|
|
const samplerNode = samplerNodes[0]
|
|
const vaeNode = vaeNodes[0]
|
|
|
|
const samplerOutput = await samplerNode.getOutput(0)
|
|
const vaeInput = await vaeNode.getInput(0)
|
|
|
|
const samplerOutputKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const vaeInputKey = getSlotKey(String(vaeNode.id), 0, true)
|
|
|
|
const samplerOutputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerOutputKey}"]`
|
|
)
|
|
const vaeInputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${vaeInputKey}"]`
|
|
)
|
|
|
|
await expect(samplerOutputLocator).toBeVisible()
|
|
await expect(vaeInputLocator).toBeVisible()
|
|
|
|
await samplerOutputLocator.dragTo(vaeInputLocator)
|
|
await 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 getCenter(vaeInputLocator)
|
|
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 samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
|
|
expect(vaeNodes.length).toBeGreaterThan(0)
|
|
|
|
const samplerNode = samplerNodes[0]
|
|
const vaeNode = vaeNodes[0]
|
|
|
|
const samplerOutput = await samplerNode.getOutput(0)
|
|
const vaeInput = await vaeNode.getInput(0)
|
|
|
|
await samplerOutput.removeLinks()
|
|
await vaeInput.removeLinks()
|
|
|
|
const samplerOutputKey = getSlotKey(String(samplerNode.id), 0, false)
|
|
const vaeInputKey = getSlotKey(String(vaeNode.id), 0, true)
|
|
|
|
const samplerOutputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerOutputKey}"]`
|
|
)
|
|
const vaeInputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${vaeInputKey}"]`
|
|
)
|
|
|
|
await expect(samplerOutputLocator).toBeVisible()
|
|
await expect(vaeInputLocator).toBeVisible()
|
|
|
|
await samplerOutputLocator.dragTo(vaeInputLocator)
|
|
await 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 getCenter(samplerOutputLocator)
|
|
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 clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
|
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
|
|
expect(clipNodes.length).toBeGreaterThan(0)
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
|
|
const clipNode = clipNodes[0]
|
|
const samplerNode = samplerNodes[0]
|
|
|
|
// Step 1: Connect CLIP's only output (index 0) to KSampler's second input (index 1)
|
|
const clipOutputKey = getSlotKey(String(clipNode.id), 0, false)
|
|
const samplerInput2ndKey = getSlotKey(String(samplerNode.id), 1, true)
|
|
|
|
const clipOutputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${clipOutputKey}"]`
|
|
)
|
|
const samplerInput2ndLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerInput2ndKey}"]`
|
|
)
|
|
|
|
await expect(clipOutputLocator).toBeVisible()
|
|
await expect(samplerInput2ndLocator).toBeVisible()
|
|
|
|
await clipOutputLocator.dragTo(samplerInput2ndLocator)
|
|
await 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 samplerInput3rdKey = getSlotKey(String(samplerNode.id), 2, true)
|
|
const samplerInput3rdLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerInput3rdKey}"]`
|
|
)
|
|
await expect(samplerInput3rdLocator).toBeVisible()
|
|
|
|
const input2Center = await getCenter(samplerInput2ndLocator)
|
|
const input3Center = await getCenter(samplerInput3rdLocator)
|
|
|
|
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 clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
|
|
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
|
|
|
|
expect(clipNodes.length).toBeGreaterThan(0)
|
|
expect(samplerNodes.length).toBeGreaterThan(0)
|
|
|
|
const clipNode = clipNodes[0]
|
|
const samplerNode = samplerNodes[0]
|
|
|
|
const clipOutputKey = getSlotKey(String(clipNode.id), 0, false)
|
|
const samplerInputSecondKey = getSlotKey(String(samplerNode.id), 1, true)
|
|
const samplerInputThirdKey = getSlotKey(String(samplerNode.id), 2, true)
|
|
|
|
const clipOutputLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${clipOutputKey}"]`
|
|
)
|
|
const samplerInputSecondLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerInputSecondKey}"]`
|
|
)
|
|
const samplerInputThirdLocator = comfyPage.page.locator(
|
|
`[data-slot-key="${samplerInputThirdKey}"]`
|
|
)
|
|
|
|
await expect(clipOutputLocator).toBeVisible()
|
|
await expect(samplerInputSecondLocator).toBeVisible()
|
|
await expect(samplerInputThirdLocator).toBeVisible()
|
|
|
|
await clipOutputLocator.dragTo(samplerInputSecondLocator)
|
|
await comfyPage.nextFrame()
|
|
await clipOutputLocator.dragTo(samplerInputThirdLocator)
|
|
await comfyPage.nextFrame()
|
|
|
|
const clipOutput = await clipNode.getOutput(0)
|
|
|
|
expect(await clipOutput.getLinkCount()).toBe(2)
|
|
|
|
const outputCenter = await getCenter(clipOutputLocator)
|
|
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()
|
|
}
|
|
if (shiftHeld) {
|
|
await comfyPage.page.keyboard.up('Shift')
|
|
}
|
|
}
|
|
})
|
|
})
|