Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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
37 changes: 37 additions & 0 deletions browser_tests/fixtures/VueNodeHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/**
* Vue Node Test Helpers
*/
import { expect } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'

import { TestIds } from '@e2e/fixtures/selectors'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { toNodeId } from '@/types/nodeId'
import { VueNodeFixture } from '@e2e/fixtures/utils/vueNodeFixtures'

const GRAPH_SIZE_GROWTH: [number, number] = [90, 100]

export class VueNodeHelpers {
/**
* Get locator for all Vue node components in the DOM
Expand Down Expand Up @@ -84,6 +87,40 @@ export class VueNodeHelpers {
)
}

async expectGraphSizeGrowth(nodeId: string, label: string): Promise<void> {
const node = this.getNodeLocator(nodeId)
const before = await node.evaluate((element) => [
element instanceof HTMLElement ? element.offsetWidth : 0,
element instanceof HTMLElement ? element.offsetHeight : 0
])
expect(before[0], `${label}: node has a rendered width`).toBeGreaterThan(0)
expect(before[1], `${label}: node has a rendered height`).toBeGreaterThan(0)

await this.page.evaluate(
({ id, growth }) => {
const node = window.app?.canvas.graph?.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found`)

node.setSize([node.size[0] + growth[0], node.size[1] + growth[1]])
},
{ id: toNodeId(nodeId), growth: GRAPH_SIZE_GROWTH }
)

await expect
.poll(
() =>
node.evaluate((element) => [
element instanceof HTMLElement ? element.offsetWidth : 0,
element instanceof HTMLElement ? element.offsetHeight : 0
]),
{ message: label }
)
.toEqual([
before[0] + GRAPH_SIZE_GROWTH[0],
before[1] + GRAPH_SIZE_GROWTH[1]
])
}

/**
* Select a specific Vue node by ID
*/
Expand Down
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 = {
inputs: Point[]
outputs: Point[]
pos: Point
size: Point
}

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
80 changes: 56 additions & 24 deletions browser_tests/tests/appModeBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import {
comfyPageFixture as test,
comfyExpect as expect
} from '@e2e/fixtures/ComfyPage'
import {
dismissErrorOverlay,
enableErrorsOverlay
} from '@e2e/fixtures/helpers/ErrorsTabHelper'
import { ExecutionHelper } from '@e2e/fixtures/helpers/ExecutionHelper'

test.describe('App mode builder selection', () => {
test.beforeEach(async ({ comfyPage }) => {
Expand Down Expand Up @@ -94,33 +99,60 @@ test.describe('App mode builder selection', () => {
}
)

test('Can not select nodes with errors or notes', async ({ comfyPage }) => {
//Manually set error state on checkpoint loader
//Shouldn't be needed on ci, but has spotty reliability
await comfyPage.page.evaluate(() => (graph!.nodes[6].has_errors = true))
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)

const items = comfyPage.appMode.select.inputItems
await comfyPage.appMode.enterBuilder()
await comfyPage.appMode.steps.goToInputs()
await expect(items).toHaveCount(0)

await comfyPage.appMode.select.selectInputWidget(
'Load Checkpoint',
'ckpt_name'
)
await expect(items).toHaveCount(0)
test(
'Can not select a node with an error',
{ tag: '@vue-nodes' },
async ({ comfyPage }) => {
test.slow()
await comfyPage.workflow.loadWorkflow('default')
await enableErrorsOverlay(comfyPage)

const [checkpointLoader] =
await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint')
await new ExecutionHelper(comfyPage).mockValidationFailure({
[String(checkpointLoader.id)]: {
class_type: 'CheckpointLoaderSimple',
dependent_outputs: [],
errors: [
{
type: 'value_not_in_list',
message: 'Value not in list',
details: '',
extra_info: { input_name: 'ckpt_name' }
}
]
}
})
await comfyPage.runButton.click()
await dismissErrorOverlay(comfyPage)

const items = comfyPage.appMode.select.inputItems
await comfyPage.appMode.enterBuilder()
await comfyPage.appMode.steps.goToInputs()
await comfyPage.appMode.select.selectInputWidget(
'Load Checkpoint',
'ckpt_name'
)

await expect(items).toHaveCount(0)
}
)

await comfyPage.workflow.loadWorkflow('nodes/note_nodes')
await comfyPage.appMode.enterBuilder()
await comfyPage.appMode.steps.goToInputs()
await expect(items).toHaveCount(0)
test(
'Can not select note nodes',
{ tag: '@vue-nodes' },
async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('nodes/note_nodes')

await comfyPage.appMode.select.selectInputWidget('Note', 'text')
await comfyPage.appMode.select.selectInputWidget('Markdown Note', 'text')
const items = comfyPage.appMode.select.inputItems
await comfyPage.appMode.enterBuilder()
await comfyPage.appMode.steps.goToInputs()
await comfyPage.appMode.select.selectInputWidget('Note', 'text')
await comfyPage.appMode.select.selectInputWidget('Markdown Note', 'text')

await expect(items).toHaveCount(0)
})
await expect(items).toHaveCount(0)
}
)

test('Marks canvas readOnly', async ({ comfyPage }) => {
await comfyPage.searchBoxV2.openByDoubleClickCanvas()
Expand Down
46 changes: 46 additions & 0 deletions browser_tests/tests/copyPaste.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,52 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})

test.describe(
'Pinned node copy and paste',
{ tag: ['@node', '@workflow'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
})

test.afterEach(async ({ comfyPage }) => {
await comfyPage.canvasOps.resetView()
})

test('lands at the cursor', async ({ comfyPage }) => {
const [node] = await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
if (!node) throw new Error('KSampler node not found')
await node.centerOnNode()
await node.clickContextMenuOption('Pin')
await comfyPage.contextMenu.waitForHidden()
await expect.poll(() => node.isPinned()).toBe(true)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this

Suggested change
await expect.poll(() => node.isPinned()).toBe(true)
await expect.poll(node).toBePinned()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 44abed6 using the existing retrying custom matcher: await expect(node).toBePinned(). The spec now imports comfyExpect so the matcher is available.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching to the custom assertion though, good catch.


const nodeType = await node.getType()
const originalNodes = await comfyPage.nodeOps.getNodeRefsByType(nodeType)
const originalIds = new Set(originalNodes.map(({ id }) => id))
const originalGraphNodeCount =
await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.page.mouse.move(400, 300)
await comfyPage.nextFrame()
await comfyPage.clipboard.copy(comfyPage.canvas)
await comfyPage.clipboard.paste(comfyPage.canvas)

await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(originalGraphNodeCount + 1)
const nodes = await comfyPage.nodeOps.getNodeRefsByType(nodeType)
const pasted = nodes.find(({ id }) => !originalIds.has(id))
if (!pasted) throw new Error('Pasted node not found')
await expect
.poll(async () => {
const { x, y } = await pasted.getPosition()
return Math.hypot(x - 400, y - 300)
})
.toBeLessThanOrEqual(2)
})
}
)

test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.canvas.click({
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)
}
)
})
Loading
Loading