Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,12 @@ export class ComfyPage {
this.templatesDialog = new TemplatesDialog(page)
this.titleEditor = new TitleEditor(page)
this.mediaLightbox = new MediaLightbox(page)
this.vueNodes = new VueNodeHelpers(page)
this.settings = new SettingsHelper(page)
this.vueNodes = new VueNodeHelpers(page, this.settings)
this.appMode = new AppModeHelper(this)
this.subgraph = new SubgraphHelper(this)
this.canvasOps = new CanvasHelper(page, this.canvas, this.resetViewButton)
this.nodeOps = new NodeOperationsHelper(this)
this.settings = new SettingsHelper(page)
this.keyboard = new KeyboardHelper(page, this.canvas)
this.clipboard = new ClipboardHelper(this.keyboard, page)
this.workflow = new WorkflowHelper(this)
Expand Down
47 changes: 46 additions & 1 deletion browser_tests/fixtures/VueNodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import type { Locator, Page } from '@playwright/test'

import { TestIds } from '@e2e/fixtures/selectors'
import type { SettingsHelper } from '@e2e/fixtures/helpers/SettingsHelper'
import { nextFrame } from '@e2e/fixtures/utils/timing'
import type { Point } from '@/lib/litegraph/src/interfaces'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { toNodeId } from '@/types/nodeId'
import { VueNodeFixture } from '@e2e/fixtures/utils/vueNodeFixtures'
Expand All @@ -18,7 +21,10 @@ export class VueNodeHelpers {
*/
public readonly selectedNodes: Locator

constructor(private page: Page) {
constructor(
private page: Page,
private settings: SettingsHelper
) {
this.nodes = page.locator('[data-node-id]')
this.selectedNodes = page.locator(
'[data-node-id].outline-node-component-outline'
Expand Down Expand Up @@ -84,6 +90,45 @@ export class VueNodeHelpers {
)
}

async layoutNodesAndEnableVirtualization(
getPosition: (nodeId: string, index: number) => Point
): Promise<string[]> {
const nodeIds = await this.page.evaluate(() =>
window.app!.graph.nodes.map((node) => String(node.id))
)
const entries = nodeIds.map(
(nodeId, index) => [nodeId, getPosition(nodeId, index)] as const
)

await this.page.evaluate((entries) => {
const graph = window.app!.graph
if (graph.nodes.length !== entries.length) {
throw new Error('Graph nodes changed while applying test layout')
}
const positionById = new Map(entries)
graph.nodes.forEach((node) => {
const position = positionById.get(String(node.id))
if (!position) throw new Error(`Missing position for node ${node.id}`)
node.setPos(...position)
node.updateArea()
})
graph.groups.forEach((group) => group.recomputeInsideNodes())

const canvas = window.app!.canvas
canvas.ds.offset[0] = 0
canvas.ds.offset[1] = 0
canvas.ds.scale = 1
canvas.setDirty(true, true)
}, entries)
await nextFrame(this.page)
await this.settings.setSetting(
'Comfy.VueNodes.ViewportVirtualization',
true
)

return nodeIds
}

/**
* Select a specific Vue node by ID
*/
Expand Down
288 changes: 288 additions & 0 deletions browser_tests/tests/vueNodes/viewportVirtualization.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '@e2e/fixtures/ComfyPage'
import { toNodeId } from '@/types/nodeId'

test.describe(
'Vue node viewport virtualization',
{ tag: ['@vue-nodes', '@canvas'] },
() => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.VueNodes.ViewportVirtualization',
false
)
await comfyPage.settings.setSetting('Comfy.VueNodes.LowZoomLOD', true)
await comfyPage.settings.setSetting('Comfy.VueNodes.FullDetailZoom', 95)
await comfyPage.canvasOps.resetView()
})

test('hydrates all nodes, then swaps to the settled viewport', async ({
comfyPage
}) => {
const graphNodeIds =
await comfyPage.vueNodes.layoutNodesAndEnableVirtualization(
(_nodeId, index) => [
index < 2 ? 100 + index * 500 : 1800 + (index - 2) * 1800,
100
]
)

const firstViewportIds = graphNodeIds.slice(0, 2)
await expect
.poll(() => comfyPage.vueNodes.getNodeIds())
.toEqual(firstViewportIds)

await comfyPage.page.evaluate(() => {
const canvas = window.app!.canvas
canvas.ds.offset[0] = -1800
canvas.setDirty(true, true)
})
await comfyPage.nextFrame()

await expect
.poll(() => comfyPage.vueNodes.getNodeIds())
.toEqual([graphNodeIds[2]])
expect(
await comfyPage.page.evaluate(() => window.app!.graph.nodes.length)
).toBe(graphNodeIds.length)
})

test('keeps a multi-node drag mount set frozen during edge auto-pan', async ({
comfyPage
}) => {
const graphNodeIds =
await comfyPage.vueNodes.layoutNodesAndEnableVirtualization(
(_nodeId, index) => [
index < 2 ? 100 + index * 500 : 1800 + (index - 2) * 1800,
100
]
)

const initialIds = graphNodeIds.slice(0, 2)
await expect
.poll(() => comfyPage.vueNodes.getNodeIds())
.toEqual(initialIds)
await comfyPage.vueNodes.selectNodes(initialIds)

const dragNode = comfyPage.vueNodes.getNodeLocator(initialIds[0])
const header = dragNode.locator('.lg-node-header')
const headerBox = await header.boundingBox()
const canvasBox = await comfyPage.canvas.boundingBox()
if (!headerBox || !canvasBox) throw new Error('Drag geometry unavailable')

const initialOffset = await comfyPage.canvasOps.getOffset()
await comfyPage.page.mouse.move(
headerBox.x + headerBox.width / 2,
headerBox.y + headerBox.height / 2
)
await comfyPage.page.mouse.down()
try {
await comfyPage.page.mouse.move(
canvasBox.x + canvasBox.width - 2,
canvasBox.y + canvasBox.height / 2,
{ steps: 10 }
)
await expect
.poll(() => comfyPage.canvasOps.getOffset())
.not.toEqual(initialOffset)
expect(await comfyPage.vueNodes.getNodeIds()).toEqual(initialIds)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} finally {
await comfyPage.page.mouse.up()
}
})

test('keeps a group drag mounted until the interaction ends', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('groups/oversized_group')
await comfyPage.page.evaluate(() => {
const group = window.app!.graph.groups[0]
group.pos = [50, 50]
group.size = [900, 825]
})
const [nodeId] =
await comfyPage.vueNodes.layoutNodesAndEnableVirtualization(() => [
100, 100
])
if (!nodeId) throw new Error('Expected a node in the group workflow')
await expect.poll(() => comfyPage.vueNodes.getNodeIds()).toEqual([nodeId])

await comfyPage.page.evaluate(() => {
const canvas = window.app!.canvas
const node = window.app!.graph.nodes[0]
canvas.isDragging = true
node.setPos(3100, 100)
node.updateArea()
canvas.setDirty(true, true)
})
await expect
.poll(() => comfyPage.vueNodes.getNodeIds(), { timeout: 1_000 })
.toEqual([nodeId])
Comment thread
coderabbitai[bot] marked this conversation as resolved.

await comfyPage.page.evaluate(() => {
window.app!.canvas.isDragging = false
window.dispatchEvent(new PointerEvent('pointerup'))
})
await expect.poll(() => comfyPage.vueNodes.getNodeIds()).toEqual([])
})

test('hydrates a node pasted into a distant settled viewport', async ({
comfyPage
}) => {
const [sourceId] =
await comfyPage.vueNodes.layoutNodesAndEnableVirtualization(
(_nodeId, index) => [
index === 0 ? 100 : 4000 + (index - 1) * 1800,
100
]
)
if (!sourceId) throw new Error('Expected a source node')

await expect
.poll(() => comfyPage.vueNodes.getNodeIds())
.toEqual([sourceId])
await comfyPage.vueNodes.getNodeLocator(sourceId).click()
await comfyPage.clipboard.copy()
await comfyPage.page.evaluate(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
})

await comfyPage.page.evaluate(() => {
const canvas = window.app!.canvas
canvas.ds.offset[0] = -1800
canvas.setDirty(true, true)
})
const canvasBox = await comfyPage.canvas.boundingBox()
if (!canvasBox) throw new Error('Canvas geometry unavailable')
await comfyPage.page.mouse.move(
canvasBox.x + canvasBox.width / 2,
canvasBox.y + canvasBox.height / 2
)
await expect.poll(() => comfyPage.vueNodes.getNodeIds()).toEqual([])

const graphNodeCount = await comfyPage.page.evaluate(
() => window.app!.graph.nodes.length
)
await comfyPage.clipboard.paste()
await expect
.poll(() =>
comfyPage.page.evaluate(() => window.app!.graph.nodes.length)
)
.toBe(graphNodeCount + 1)
await expect.poll(() => comfyPage.vueNodes.getNodeIds()).toHaveLength(1)
})

test('mounts a distant link target after auto-pan pauses', async ({
comfyPage
}) => {
const ids = await comfyPage.page.evaluate(() => {
const graph = window.app!.graph
const source = graph.nodes.find(
(node) => node.getTitle() === 'Load Diffusion Model'
)!
const target = graph.nodes.find(
(node) => node.getTitle() === 'KSampler'
)!
const targetSlot = target.findInputSlot('model')
if (targetSlot >= 0) target.disconnectInput(targetSlot)
return { source: String(source.id), target: String(target.id) }
})
await comfyPage.vueNodes.layoutNodesAndEnableVirtualization(
(nodeId, index) => {
if (nodeId === ids.source) return [100, 100]
if (nodeId === ids.target) return [1800, 100]
return [4000 + index * 1800, 100]
}
)

await expect(comfyPage.vueNodes.getNodeLocator(ids.source)).toBeVisible()
await expect(comfyPage.vueNodes.getNodeLocator(ids.target)).toHaveCount(0)

const source = await comfyPage.vueNodes.getFixtureByTitle(
'Load Diffusion Model'
)
const sourceSlot = source.getSlot('MODEL')
const sourceBox = await sourceSlot.boundingBox()
const canvasBox = await comfyPage.canvas.boundingBox()
if (!sourceBox || !canvasBox) throw new Error('Link geometry unavailable')

await comfyPage.page.mouse.move(
sourceBox.x + sourceBox.width / 2,
sourceBox.y + sourceBox.height / 2
)
await comfyPage.page.mouse.down()
try {
await comfyPage.page.mouse.move(
canvasBox.x + canvasBox.width - 2,
canvasBox.y + canvasBox.height / 2
)
await expect
.poll(async () => (await comfyPage.canvasOps.getOffset())[0])
.toBeLessThan(-1000)

await comfyPage.page.mouse.move(
canvasBox.x + canvasBox.width / 2,
canvasBox.y + canvasBox.height / 2
)
await expect(
comfyPage.vueNodes.getNodeLocator(ids.target)
).toBeVisible()

const target = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
const targetSlot = target.getSlot('model')
const targetBox = await targetSlot.boundingBox()
if (!targetBox) throw new Error('Target slot geometry unavailable')
await comfyPage.page.mouse.move(
targetBox.x + targetBox.width / 2,
targetBox.y + targetBox.height / 2
)
} finally {
await comfyPage.page.mouse.up()
}

await expect
.poll(() =>
comfyPage.page.evaluate((targetId) => {
const target = window.app!.graph.getNodeById(targetId)
const slot = target?.findInputSlot('model') ?? -1
return slot >= 0 ? target?.inputs[slot].link : null
}, toNodeId(ids.target))
)
.not.toBeNull()
})

test('switches paint detail strictly below the configured zoom', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.LowZoomLOD', true)
await comfyPage.settings.setSetting('Comfy.VueNodes.FullDetailZoom', 95)
const widget = comfyPage.vueNodes.nodes.locator('.lg-node-widget').first()

await comfyPage.page.evaluate(() => {
window.app!.canvas.ds.scale = 0.949
window.app!.canvas.setDirty(true, true)
})
await expect
.poll(() => comfyPage.page.locator('html').getAttribute('class'))
.toContain('vue-nodes-low-detail')
await expect
.poll(() => widget.evaluate((el) => getComputedStyle(el).visibility))
.toBe('hidden')

await comfyPage.page.evaluate(() => {
window.app!.canvas.ds.scale = 0.95
window.app!.canvas.setDirty(true, true)
})
await expect
.poll(() => comfyPage.page.locator('html').getAttribute('class'))
.not.toContain('vue-nodes-low-detail')
await expect
.poll(() => widget.evaluate((el) => getComputedStyle(el).visibility))
.toBe('visible')
})
}
)
Loading
Loading