Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions browser_tests/fixtures/helpers/CanvasHelper.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { expect } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'

import { DefaultGraphPositions } from '@e2e/fixtures/constants/defaultGraphPositions'
import type { Position } from '@e2e/fixtures/types'
import { nextFrame } from '@e2e/fixtures/utils/timing'
import type { RerouteId } from '@/types/rerouteId'

export class CanvasHelper {
constructor(
Expand Down Expand Up @@ -195,6 +197,30 @@ export class CanvasHelper {
}, title)
}

async expectRootReroutePositions(
expectedReroutes: Record<RerouteId, Position>
): Promise<void> {
await expect(async () => {
const reroutes = await this.page.evaluate(() => {
const graph = window.app!.canvas.graph?.rootGraph
if (!graph) throw new Error('Graph not available')
return [...graph.reroutes.values()].map((reroute) => ({
id: reroute.id,
x: reroute.pos[0],
y: reroute.pos[1]
}))
})

expect(reroutes).toHaveLength(Object.keys(expectedReroutes).length)
for (const reroute of reroutes) {
const expected = expectedReroutes[reroute.id]
if (!expected) throw new Error(`Unexpected reroute ${reroute.id}`)
expect(reroute.x).toBeCloseTo(expected.x, 1)
expect(reroute.y).toBeCloseTo(expected.y, 1)
}
}).toPass({ timeout: 5000 })
}

async getGroupPosition(title: string): Promise<Position> {
const pos = await this.page.evaluate((title) => {
const groups = window.app!.graph.groups
Expand Down
39 changes: 39 additions & 0 deletions browser_tests/tests/nodeReplacement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,43 @@ test.describe('Node replacement', { tag: ['@node', '@ui'] }, () => {
})
})
}

test(
'Replacement keeps its position when enabling Vue Nodes',
{ tag: ['@vue-nodes'] },
async ({ comfyPage }) => {
test.slow()
await setupNodeReplacement(comfyPage, mockNodeReplacementsSingle)
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/node_replacement_simple'
)
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
await comfyPage.nextFrame()

await getSwapNodesGroup(comfyPage.page)
.getByRole('button', { name: /replace node/i })
.click()

const [ksampler] = await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
await ksampler.dragBy({ x: 120, y: 90 })
await comfyPage.nextFrame()
const draggedTitlePosition = await ksampler.getTitlePosition()

await comfyPage.menu.topbar.setVueNodesEnabled(true)
await comfyPage.vueNodes.waitForNodes()

const { header } = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await expect
.poll(async () => {
const box = await header.boundingBox()
if (!box) return Number.POSITIVE_INFINITY
return Math.max(
Math.abs(box.x + box.width / 2 - draggedTitlePosition.x),
Math.abs(box.y + box.height / 2 - draggedTitlePosition.y)
)
})
.toBeLessThanOrEqual(2)
}
)
})
49 changes: 49 additions & 0 deletions browser_tests/tests/performance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,55 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
)
})

test('large graph legacy node drag', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('large-graph-workflow')

// Legacy drags write to layoutStore every frame because registration is
// renderer-independent.
const nodePos = await comfyPage.page.evaluate(() => {
const app = window.app
if (!app) throw new Error('window.app is not available')

const { canvas } = app
const node = app.graph.nodes[0]
if (!node) throw new Error('Graph has no nodes')

canvas.ds.scale = 1
canvas.centerOnNode(node)
const [x, y] = app.canvasPosToClientPos(node.pos)
return { id: node.id, x, y, graphX: node.pos[0] }
})
await comfyPage.nextFrame()

await comfyPage.perf.startMeasuring()

await comfyPage.page.mouse.move(nodePos.x + 40, nodePos.y + 10)
await comfyPage.page.mouse.down()
for (let i = 0; i < 60; i++) {
await comfyPage.page.mouse.move(
nodePos.x + 40 + i * 4,
nodePos.y + 10 + i * 2
)
await comfyPage.nextFrame()
}
await comfyPage.page.mouse.up()

const m = await comfyPage.perf.stopMeasuring('legacy-node-drag')
recordMeasurement(m)

// Verify the measured interaction was a node drag, not a canvas pan.
const movedX = await comfyPage.page.evaluate((id) => {
const node = window.app?.graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found`)
return node.pos[0]
}, nodePos.id)
expect(movedX).not.toBeCloseTo(nodePos.graphX, 0)

console.log(
`Legacy node drag: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts, ${m.taskDurationMs.toFixed(1)}ms task`
)
})

test('large graph zoom interaction', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('large-graph-workflow')

Expand Down
208 changes: 208 additions & 0 deletions browser_tests/tests/vueNodes/layout/rendererToggleGeometry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { fitToViewInstant } from '@e2e/fixtures/utils/fitToView'
import type { Point } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/types/nodeId'
import { toNodeId } from '@/types/nodeId'

const LEGACY_TITLE_HEIGHT = 30

interface NodeGeometry {
pos: Point
size: Point
inputs: Point[]
outputs: Point[]
}

/**
* Read slots through the accessors used by `drawConnections`; raw fields
* bypass the store projection under test.
*/
async function readGeometry(
comfyPage: ComfyPage,
nodeId: NodeId
): Promise<NodeGeometry> {
return comfyPage.page.evaluate((id): NodeGeometry => {
const node = window.app?.canvas.graph?.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found`)

return {
pos: [node.pos[0], node.pos[1]],
size: [node.size[0], node.size[1]],
inputs: node.inputs.map((_, i) => node.getInputPos(i)),
outputs: node.outputs.map((_, i) => node.getOutputPos(i))
}
}, nodeId)
}

function slotsOf(geometry: NodeGeometry): Point[] {
return [...geometry.inputs, ...geometry.outputs]
}

function expectSlotsTrackedNode(after: NodeGeometry, before: NodeGeometry) {
const dx = after.pos[0] - before.pos[0]
const dy = after.pos[1] - before.pos[1]
expect(Math.abs(dx) + Math.abs(dy), 'drag moved the node').toBeGreaterThan(1)

const beforeSlots = slotsOf(before)
const afterSlots = slotsOf(after)
expect(afterSlots, 'slot count after drag').toHaveLength(beforeSlots.length)
afterSlots.forEach(([x, y], i) => {
expect(x, `slot ${i} x tracked the node`).toBeCloseTo(
beforeSlots[i][0] + dx,
0
)
expect(y, `slot ${i} y tracked the node`).toBeCloseTo(
beforeSlots[i][1] + dy,
0
)
})
}

function expectGeometryPreserved(
actual: NodeGeometry,
reference: NodeGeometry,
label: string
) {
expect(actual.pos[0], `${label}: x`).toBeCloseTo(reference.pos[0], 0)
expect(actual.pos[1], `${label}: y`).toBeCloseTo(reference.pos[1], 0)
expect(actual.size, `${label}: size`).toEqual(reference.size)

const referenceSlots = slotsOf(reference)
const actualSlots = slotsOf(actual)
expect(actualSlots, `${label}: slot count`).toHaveLength(
referenceSlots.length
)
actualSlots.forEach(([x, y], i) => {
expect(x, `${label}: slot ${i} x`).toBeCloseTo(referenceSlots[i][0], 0)
expect(y, `${label}: slot ${i} y`).toBeCloseTo(referenceSlots[i][1], 0)
})
}

/**
* Renderers compute different slot offsets, so require slots only to remain
* within their node bounds.
*/
function expectSlotsOnNode(geometry: NodeGeometry, label: string) {
const MARGIN = 20
const [x, y] = geometry.pos
const [width, height] = geometry.size

slotsOf(geometry).forEach(([slotX, slotY], i) => {
expect(slotX, `${label}: slot ${i} x within node`).toBeGreaterThanOrEqual(
x - MARGIN
)
expect(slotX, `${label}: slot ${i} x within node`).toBeLessThanOrEqual(
x + width + MARGIN
)
expect(slotY, `${label}: slot ${i} y within node`).toBeGreaterThanOrEqual(
y - LEGACY_TITLE_HEIGHT - MARGIN
)
expect(slotY, `${label}: slot ${i} y within node`).toBeLessThanOrEqual(
y + height + MARGIN
)
})
}

async function setVueMode(comfyPage: ComfyPage, enabled: boolean) {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', enabled)
if (enabled) await comfyPage.vueNodes.waitForNodes()
await comfyPage.nextFrame()
}

test.describe('Renderer toggle geometry', { tag: ['@vue-nodes'] }, () => {
test('slot geometry survives a Vue to legacy round trip', async ({
comfyPage
}) => {
const nodeId = toNodeId(
await comfyPage.vueNodes.getNodeIdByTitle('KSampler')
)

const before = await readGeometry(comfyPage, nodeId)
expect(
slotsOf(before).length,
'fixture node must have slots for this test to mean anything'
).toBeGreaterThan(0)

const { header } = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
const headerBox = await header.boundingBox()
if (!headerBox) throw new Error('KSampler header not found')

const startX = headerBox.x + headerBox.width / 2
const startY = headerBox.y + headerBox.height / 2
await comfyPage.page.mouse.move(startX, startY)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(startX + 120, startY + 90, { steps: 10 })
await comfyPage.page.mouse.up()
await comfyPage.nextFrame()

const moved = await readGeometry(comfyPage, nodeId)
expectSlotsTrackedNode(moved, before)

await setVueMode(comfyPage, false)

const legacyGeometry = await readGeometry(comfyPage, nodeId)
expect(legacyGeometry.pos[0], 'legacy x').toBeCloseTo(moved.pos[0], 0)
expect(legacyGeometry.pos[1], 'legacy y').toBeCloseTo(moved.pos[1], 0)
expect(legacyGeometry.size, 'legacy size').toEqual(moved.size)
expectSlotsOnNode(legacyGeometry, 'after switching to legacy')

await setVueMode(comfyPage, true)

expectGeometryPreserved(
await readGeometry(comfyPage, nodeId),
moved,
'after switching back to Vue'
)
})

test(
'preserves frontmost order after a legacy drag',
{ tag: ['@node'] },
async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple')
await setVueMode(comfyPage, false)
await fitToViewInstant(comfyPage)

const [ksampler] = await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
const [clip] = await comfyPage.nodeOps.getNodeRefsByTitle(
'CLIP Text Encode (Prompt)'
)
const ksamplerPosition = await ksampler.getPosition()
const clipPosition = await clip.getPosition()

await ksampler.dragBy({
x: clipPosition.x - ksamplerPosition.x,
y: clipPosition.y - ksamplerPosition.y
})
await comfyPage.nextFrame()

const lastNodeId = await comfyPage.page.evaluate(
() => window.app?.graph.nodes.at(-1)?.id
)
expect(lastNodeId, 'KSampler is frontmost after the legacy drag').toBe(
ksampler.id
)

await setVueMode(comfyPage, true)
await expect(comfyPage.vueNodes.nodes).toHaveCount(3)

const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode')
await expect
.poll(async () => {
const ksamplerZIndex = await ksamplerNode.evaluate((node) =>
Number(getComputedStyle(node).zIndex)
)
const clipZIndex = await clipNode.evaluate((node) =>
Number(getComputedStyle(node).zIndex)
)
return ksamplerZIndex - clipZIndex
})
.toBeGreaterThan(0)
}
)
})
Loading
Loading