Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6001bbc
test: add ECS migration regression coverage
ampagent Aug 15, 2026
4842171
test: make migration regression coverage behavior-focused
ampagent Aug 15, 2026
da6e1f7
test: cover mixed node drag selection
ampagent Aug 15, 2026
94c5aad
Apply suggestions from code review
DrJKL Aug 15, 2026
3e273c6
Update browser_tests/fixtures/helpers/CanvasHelper.ts
DrJKL Aug 15, 2026
81170c5
test: prefer behavior-level ECS regression coverage
ampagent Aug 15, 2026
f3722ee
test: strengthen behavior-level regression assertions
ampagent Aug 15, 2026
9dafbcc
test: expose semantic widget error state
ampagent Aug 15, 2026
7e2945d
test: cover cyclic subgraph definition collection
ampagent Aug 15, 2026
2a8e0d5
test: add more migration-safe behavior coverage
ampagent Aug 15, 2026
37072a8
test: strengthen additional migration behavior coverage
ampagent Aug 15, 2026
f6ee63e
test: address behavior coverage review feedback
ampagent Aug 16, 2026
951a9e6
Merge remote-tracking branch 'origin/main' into test/ecs-migration-re…
ampagent Aug 17, 2026
55550fd
test: select reroute fixture through Vue node UI
ampagent Aug 17, 2026
bd1348f
test: assert touch pan behavior directly
ampagent Aug 17, 2026
d93152b
Merge remote-tracking branch 'origin/main' into test/ecs-migration-re…
ampagent Aug 17, 2026
532214f
test: create valid floating link fixture
ampagent Aug 17, 2026
36588fc
test: keep regression coverage behavioral
ampagent Aug 17, 2026
44abed6
test: use pinned node matcher
ampagent Aug 17, 2026
d79ea90
test: reuse bounds matcher for node growth
ampagent Aug 17, 2026
0019f39
Merge branch 'main' into test/ecs-migration-regression-coverage
DrJKL Aug 17, 2026
5c49957
[automated] Update test expectations
invalid-email-address Aug 17, 2026
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
112 changes: 112 additions & 0 deletions browser_tests/fixtures/helpers/CanvasHelper.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
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 { Point } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/types/nodeId'
import type { RerouteId } from '@/types/rerouteId'

type NodeGeometry = {
pos: Point
size: Point
inputs: Point[]
outputs: Point[]
Comment thread
DrJKL marked this conversation as resolved.
Outdated
}

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

async getNodeGeometry(nodeId: NodeId): Promise<NodeGeometry> {
return this.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)
}

expectSlotsTrackedNode(after: NodeGeometry, before: NodeGeometry): void {
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 = [...before.inputs, ...before.outputs]
const afterSlots = [...after.inputs, ...after.outputs]
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
)
})
}

expectNodeGeometryPreserved(
actual: NodeGeometry,
reference: NodeGeometry,
label: string
): void {
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 = [...reference.inputs, ...reference.outputs]
const actualSlots = [...actual.inputs, ...actual.outputs]
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)
})
}

expectSlotsOnNode(geometry: NodeGeometry, label: string): void {
const [x, y] = geometry.pos
const [width, height] = geometry.size
const slots = [...geometry.inputs, ...geometry.outputs]

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

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
32 changes: 32 additions & 0 deletions browser_tests/tests/nodeReplacement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,36 @@ 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 draggedPosition =
await ksampler.getProperty<[number, number]>('pos')

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

await expect
.poll(() => ksampler.getProperty<[number, number]>('pos'))
.toEqual(draggedPosition)
}
)
})
50 changes: 50 additions & 0 deletions browser_tests/tests/performance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,56 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
)
})

test('large graph legacy node drag', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
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
99 changes: 99 additions & 0 deletions browser_tests/tests/vueNodes/layout/rendererToggleGeometry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '@e2e/fixtures/ComfyPage'
import { fitToViewInstant } from '@e2e/fixtures/utils/fitToView'
import { toNodeId } from '@/types/nodeId'

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 comfyPage.canvasOps.getNodeGeometry(nodeId)
expect(
before.inputs.length + before.outputs.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 comfyPage.canvasOps.getNodeGeometry(nodeId)
comfyPage.canvasOps.expectSlotsTrackedNode(moved, before)

await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
await comfyPage.nextFrame()

const legacyGeometry = await comfyPage.canvasOps.getNodeGeometry(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)
comfyPage.canvasOps.expectSlotsOnNode(
legacyGeometry,
'after switching to legacy'
)

await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()

comfyPage.canvasOps.expectNodeGeometryPreserved(
await comfyPage.canvasOps.getNodeGeometry(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 comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', false)
await comfyPage.nextFrame()
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()

await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
await expect(comfyPage.vueNodes.nodes).toHaveCount(3)

const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect
.poll(async () => {
const ksamplerZIndex = await ksamplerNode.evaluate((node) =>
Number(getComputedStyle(node).zIndex)
)
const zIndices = await comfyPage.vueNodes.nodes.evaluateAll((nodes) =>
nodes.map((node) => Number(getComputedStyle(node).zIndex))
)
return zIndices.filter((zIndex) => zIndex >= ksamplerZIndex).length
})
.toBe(1)
}
)
})
Loading
Loading