Skip to content

Commit 893409d

Browse files
benceruleanlugithub-actions
andauthored
Add playwright tests for links and slots in vue nodes mode (#5668)
Tests added - Should show a link dragging out from a slot when dragging on a slot - Should create a link when dropping on a compatible slot - Should not create a link when dropping on an incompatible slot(s) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5668-Add-playwright-tests-for-links-and-slots-in-vue-nodes-mode-2736d73d36508188a47dceee5d1a11e5) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <[email protected]>
1 parent df2fda6 commit 893409d

File tree

5 files changed

+327
-1
lines changed

5 files changed

+327
-1
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4}

browser_tests/helpers/fitToView.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { ReadOnlyRect } from '../../src/lib/litegraph/src/interfaces'
2+
import type { ComfyPage } from '../fixtures/ComfyPage'
3+
4+
interface FitToViewOptions {
5+
selectionOnly?: boolean
6+
zoom?: number
7+
padding?: number
8+
}
9+
10+
/**
11+
* Instantly fits the canvas view to graph content without waiting for UI animation.
12+
*
13+
* Lives outside the shared fixture to keep the default ComfyPage interactions user-oriented.
14+
*/
15+
export async function fitToViewInstant(
16+
comfyPage: ComfyPage,
17+
options: FitToViewOptions = {}
18+
) {
19+
const { selectionOnly = false, zoom = 0.75, padding = 10 } = options
20+
21+
const rectangles = await comfyPage.page.evaluate<
22+
ReadOnlyRect[] | null,
23+
{ selectionOnly: boolean }
24+
>(
25+
({ selectionOnly }) => {
26+
const app = window['app']
27+
if (!app?.canvas) return null
28+
29+
const canvas = app.canvas
30+
const items = (() => {
31+
if (selectionOnly && canvas.selectedItems?.size) {
32+
return Array.from(canvas.selectedItems)
33+
}
34+
try {
35+
return Array.from(canvas.positionableItems ?? [])
36+
} catch {
37+
return []
38+
}
39+
})()
40+
41+
if (!items.length) return null
42+
43+
const rects: ReadOnlyRect[] = []
44+
45+
for (const item of items) {
46+
const rect = item?.boundingRect
47+
if (!rect) continue
48+
49+
const x = Number(rect[0])
50+
const y = Number(rect[1])
51+
const width = Number(rect[2])
52+
const height = Number(rect[3])
53+
54+
rects.push([x, y, width, height] as const)
55+
}
56+
57+
return rects.length ? rects : null
58+
},
59+
{ selectionOnly }
60+
)
61+
62+
if (!rectangles || rectangles.length === 0) return
63+
64+
let minX = Infinity
65+
let minY = Infinity
66+
let maxX = -Infinity
67+
let maxY = -Infinity
68+
69+
for (const [x, y, width, height] of rectangles) {
70+
minX = Math.min(minX, Number(x))
71+
minY = Math.min(minY, Number(y))
72+
maxX = Math.max(maxX, Number(x) + Number(width))
73+
maxY = Math.max(maxY, Number(y) + Number(height))
74+
}
75+
76+
const hasFiniteBounds =
77+
Number.isFinite(minX) &&
78+
Number.isFinite(minY) &&
79+
Number.isFinite(maxX) &&
80+
Number.isFinite(maxY)
81+
82+
if (!hasFiniteBounds) return
83+
84+
const bounds: ReadOnlyRect = [
85+
minX - padding,
86+
minY - padding,
87+
maxX - minX + 2 * padding,
88+
maxY - minY + 2 * padding
89+
]
90+
91+
await comfyPage.page.evaluate(
92+
({ bounds, zoom }) => {
93+
const app = window['app']
94+
if (!app?.canvas) return
95+
96+
const canvas = app.canvas
97+
canvas.ds.fitToBounds(bounds, { zoom })
98+
canvas.setDirty(true, true)
99+
},
100+
{ bounds, zoom }
101+
)
102+
103+
await comfyPage.nextFrame()
104+
}

browser_tests/tests/vueNodes/NodeHeader.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
66

77
test.describe('NodeHeader', () => {
88
test.beforeEach(async ({ comfyPage }) => {
9-
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled')
9+
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
1010
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
1111
await comfyPage.setSetting('Comfy.EnableTooltips', true)
1212
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import type { Locator } from '@playwright/test'
2+
3+
import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier'
4+
import {
5+
comfyExpect as expect,
6+
comfyPageFixture as test
7+
} from '../../fixtures/ComfyPage'
8+
import { fitToViewInstant } from '../../helpers/fitToView'
9+
10+
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
11+
const box = await locator.boundingBox()
12+
if (!box) throw new Error('Slot bounding box not available')
13+
return {
14+
x: box.x + box.width / 2,
15+
y: box.y + box.height / 2
16+
}
17+
}
18+
19+
test.describe('Vue Node Link Interaction', () => {
20+
test.beforeEach(async ({ comfyPage }) => {
21+
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
22+
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
23+
await comfyPage.setup()
24+
await comfyPage.loadWorkflow('vueNodes/simple-triple')
25+
await comfyPage.vueNodes.waitForNodes()
26+
await fitToViewInstant(comfyPage)
27+
})
28+
29+
test('should show a link dragging out from a slot when dragging on a slot', async ({
30+
comfyPage,
31+
comfyMouse
32+
}) => {
33+
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
34+
expect(samplerNodes.length).toBeGreaterThan(0)
35+
36+
const samplerNode = samplerNodes[0]
37+
const outputSlot = await samplerNode.getOutput(0)
38+
await outputSlot.removeLinks()
39+
await comfyPage.nextFrame()
40+
41+
const slotKey = getSlotKey(String(samplerNode.id), 0, false)
42+
const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`)
43+
await expect(slotLocator).toBeVisible()
44+
45+
const start = await getCenter(slotLocator)
46+
const canvasBox = await comfyPage.canvas.boundingBox()
47+
if (!canvasBox) throw new Error('Canvas bounding box not available')
48+
49+
// Arbitrary value
50+
const dragTarget = {
51+
x: start.x + 180,
52+
y: start.y - 140
53+
}
54+
55+
await comfyMouse.move(start)
56+
await comfyMouse.drag(dragTarget)
57+
await comfyPage.nextFrame()
58+
59+
try {
60+
await expect(comfyPage.canvas).toHaveScreenshot(
61+
'vue-node-dragging-link.png'
62+
)
63+
} finally {
64+
await comfyMouse.drop()
65+
}
66+
})
67+
68+
test('should create a link when dropping on a compatible slot', async ({
69+
comfyPage
70+
}) => {
71+
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
72+
expect(samplerNodes.length).toBeGreaterThan(0)
73+
const samplerNode = samplerNodes[0]
74+
75+
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
76+
expect(vaeNodes.length).toBeGreaterThan(0)
77+
const vaeNode = vaeNodes[0]
78+
79+
const samplerOutput = await samplerNode.getOutput(0)
80+
const vaeInput = await vaeNode.getInput(0)
81+
82+
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
83+
const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true)
84+
85+
const outputSlot = comfyPage.page.locator(
86+
`[data-slot-key="${outputSlotKey}"]`
87+
)
88+
const inputSlot = comfyPage.page.locator(
89+
`[data-slot-key="${inputSlotKey}"]`
90+
)
91+
92+
await expect(outputSlot).toBeVisible()
93+
await expect(inputSlot).toBeVisible()
94+
95+
await outputSlot.dragTo(inputSlot)
96+
await comfyPage.nextFrame()
97+
98+
expect(await samplerOutput.getLinkCount()).toBe(1)
99+
expect(await vaeInput.getLinkCount()).toBe(1)
100+
101+
const linkDetails = await comfyPage.page.evaluate((sourceId) => {
102+
const app = window['app']
103+
const graph = app?.canvas?.graph ?? app?.graph
104+
if (!graph) return null
105+
106+
const source = graph.getNodeById(sourceId)
107+
if (!source) return null
108+
109+
const linkId = source.outputs[0]?.links?.[0]
110+
if (linkId == null) return null
111+
112+
const link = graph.links[linkId]
113+
if (!link) return null
114+
115+
return {
116+
originId: link.origin_id,
117+
originSlot: link.origin_slot,
118+
targetId: link.target_id,
119+
targetSlot: link.target_slot
120+
}
121+
}, samplerNode.id)
122+
123+
expect(linkDetails).not.toBeNull()
124+
expect(linkDetails).toMatchObject({
125+
originId: samplerNode.id,
126+
originSlot: 0,
127+
targetId: vaeNode.id,
128+
targetSlot: 0
129+
})
130+
})
131+
132+
test('should not create a link when slot types are incompatible', async ({
133+
comfyPage
134+
}) => {
135+
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
136+
expect(samplerNodes.length).toBeGreaterThan(0)
137+
const samplerNode = samplerNodes[0]
138+
139+
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
140+
expect(clipNodes.length).toBeGreaterThan(0)
141+
const clipNode = clipNodes[0]
142+
143+
const samplerOutput = await samplerNode.getOutput(0)
144+
const clipInput = await clipNode.getInput(0)
145+
146+
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
147+
const inputSlotKey = getSlotKey(String(clipNode.id), 0, true)
148+
149+
const outputSlot = comfyPage.page.locator(
150+
`[data-slot-key="${outputSlotKey}"]`
151+
)
152+
const inputSlot = comfyPage.page.locator(
153+
`[data-slot-key="${inputSlotKey}"]`
154+
)
155+
156+
await expect(outputSlot).toBeVisible()
157+
await expect(inputSlot).toBeVisible()
158+
159+
await outputSlot.dragTo(inputSlot)
160+
await comfyPage.nextFrame()
161+
162+
expect(await samplerOutput.getLinkCount()).toBe(0)
163+
expect(await clipInput.getLinkCount()).toBe(0)
164+
165+
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
166+
const app = window['app']
167+
const graph = app?.canvas?.graph ?? app?.graph
168+
if (!graph) return 0
169+
170+
const source = graph.getNodeById(sourceId)
171+
if (!source) return 0
172+
173+
return source.outputs[0]?.links?.length ?? 0
174+
}, samplerNode.id)
175+
176+
expect(graphLinkCount).toBe(0)
177+
})
178+
179+
test('should not create a link when dropping onto a slot on the same node', async ({
180+
comfyPage
181+
}) => {
182+
const samplerNodes = await comfyPage.getNodeRefsByType('KSampler')
183+
expect(samplerNodes.length).toBeGreaterThan(0)
184+
const samplerNode = samplerNodes[0]
185+
186+
const samplerOutput = await samplerNode.getOutput(0)
187+
const samplerInput = await samplerNode.getInput(3)
188+
189+
const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false)
190+
const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true)
191+
192+
const outputSlot = comfyPage.page.locator(
193+
`[data-slot-key="${outputSlotKey}"]`
194+
)
195+
const inputSlot = comfyPage.page.locator(
196+
`[data-slot-key="${inputSlotKey}"]`
197+
)
198+
199+
await expect(outputSlot).toBeVisible()
200+
await expect(inputSlot).toBeVisible()
201+
202+
await outputSlot.dragTo(inputSlot)
203+
await comfyPage.nextFrame()
204+
205+
expect(await samplerOutput.getLinkCount()).toBe(0)
206+
expect(await samplerInput.getLinkCount()).toBe(0)
207+
208+
const graphLinkCount = await comfyPage.page.evaluate((sourceId) => {
209+
const app = window['app']
210+
const graph = app?.canvas?.graph ?? app?.graph
211+
if (!graph) return 0
212+
213+
const source = graph.getNodeById(sourceId)
214+
if (!source) return 0
215+
216+
return source.outputs[0]?.links?.length ?? 0
217+
}, samplerNode.id)
218+
219+
expect(graphLinkCount).toBe(0)
220+
})
221+
})
52.8 KB
Loading

0 commit comments

Comments
 (0)