Skip to content
Merged
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
29 changes: 22 additions & 7 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test as base, expect } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'

import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
Expand Down Expand Up @@ -1591,14 +1591,29 @@ export class ComfyPage {
return window['app'].graph.nodes
})
}
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
async waitForGraphNodes(count: number) {
await this.page.waitForFunction((count) => {
return window['app']?.canvas.graph?.nodes?.length === count
}, count)
}
Comment on lines +1594 to +1598
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

waitForGraphNodes() strict equality is likely to be flaky

Waiting for nodes.length === count is brittle if the app ever adds implicit nodes (or if async graph init briefly overshoots and settles). Prefer >= (or make “exact” vs “at least” explicit via an option).

Proposed change
-  async waitForGraphNodes(count: number) {
+  async waitForGraphNodes(count: number, opts?: { exact?: boolean }) {
     await this.page.waitForFunction((count) => {
-      return window['app']?.canvas.graph?.nodes?.length === count
-    }, count)
+      const len = window['app']?.canvas.graph?.nodes?.length
+      if (len == null) return false
+      return opts?.exact ? len === count : len >= count
+    }, count)
   }
🤖 Prompt for AI Agents
In `@browser_tests/fixtures/ComfyPage.ts` around lines 1594 - 1598, The test
helper waitForGraphNodes is using strict equality on
window['app'].canvas.graph.nodes.length === count which is brittle; update the
implementation in waitForGraphNodes to use a non-strict condition (e.g., >=
count) or add a parameter like exact: boolean to choose between exact and
at-least semantics, and use that parameter to decide whether to compare with ===
or >=; ensure the call to page.waitForFunction passes the count and the option
so the function inside the browser uses the correct comparison.

async getNodeRefsByType(
type: string,
includeSubgraph: boolean = false
): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate((type) => {
return window['app'].graph.nodes
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
}, type)
await this.page.evaluate(
({ type, includeSubgraph }) => {
const graph = (
includeSubgraph ? window['app'].canvas.graph : window['app'].graph
) as LGraph
const nodes = graph.nodes
return nodes
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
},
{ type, includeSubgraph }
)
Comment on lines +1599 to +1616
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against missing graph when includeSubgraph is true

If window['app'].canvas.graph isn’t set yet (or the user isn’t inside a subgraph), this evaluate can throw. Consider returning [] when the graph is unavailable to keep helpers resilient.

).map((id: NodeId) => this.getNodeRefById(id))
)
}
Expand Down
10 changes: 10 additions & 0 deletions browser_tests/fixtures/VueNodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,14 @@ export class VueNodeHelpers {
incrementButton: widget.getByTestId('increment')
}
}

/**
* Enter the subgraph of a node.
* @param nodeId - The ID of the node to enter the subgraph of. If not provided, the first matched subgraph will be entered.
*/
async enterSubgraph(nodeId?: string): Promise<void> {
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
const editButton = locator.getByTestId('subgraph-enter-button')
await editButton.click()
}
Comment on lines +167 to +175
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

enterSubgraph() should disambiguate + wait for visibility

As written, enterSubgraph() can click an arbitrary matching button when multiple subgraph nodes exist, and it doesn’t wait for the button to be visible/enabled (potential flake).

Proposed change
   async enterSubgraph(nodeId?: string): Promise<void> {
     const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
-    const editButton = locator.getByTestId('subgraph-enter-button')
-    await editButton.click()
+    const editButton = locator.getByTestId('subgraph-enter-button').first()
+    await editButton.waitFor({ state: 'visible' })
+    await editButton.click()
   }
🤖 Prompt for AI Agents
In `@browser_tests/fixtures/VueNodeHelpers.ts` around lines 167 - 175,
enterSubgraph() can click the wrong button when many subgraph nodes exist and
may race because it doesn't wait for visibility; update enterSubgraph (and the
locator built by getNodeLocator when nodeId is provided) to scope to the
specific node when nodeId is passed and to explicitly pick the first matching
enter button when nodeId is omitted (e.g., use the locator.first() variant),
then wait for the button to be visible/ready before calling click (use the
locator waitFor/visibility check on the 'subgraph-enter-button' locator prior to
click).

}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ test.describe('Vue Node Link Interaction', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
// await comfyPage.setup()
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove commented-out code instead of leaving it commented.

Dead code should be removed rather than commented out. If this setup call is truly no longer needed (perhaps because loadWorkflow handles it internally), delete the line entirely. Version control preserves history if it needs to be restored.

Proposed fix
-    // await comfyPage.setup()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// await comfyPage.setup()
🤖 Prompt for AI Agents
In @browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts at
line 105, Remove the commented-out call to comfyPage.setup() on the test line
(the dead code left as "// await comfyPage.setup()") rather than leaving it
commented; if setup is no longer required because loadWorkflow or other test
helpers handle initialization, delete the commented line entirely so the test
file contains no commented-out dead code and version control preserves history
if restoration is needed.

await comfyPage.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
Expand Down Expand Up @@ -993,4 +993,51 @@ test.describe('Vue Node Link Interaction', () => {
expect(linked).toBe(true)
})
})

test('Dragging from subgraph input connects to correct slot', async ({
comfyPage,
comfyMouse
}) => {
// Setup workflow with a KSampler node
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.waitForGraphNodes(0)
await comfyPage.executeCommand('Workspace.SearchBox.Toggle')
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await comfyPage.waitForGraphNodes(1)

// Convert the KSampler node to a subgraph
let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0]
await comfyPage.vueNodes.selectNode(String(ksamplerNode.id))
await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph')

// Enter the subgraph
await comfyPage.vueNodes.enterSubgraph()
await fitToViewInstant(comfyPage)

// Get the KSampler node inside the subgraph
ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0]
const positiveInput = await ksamplerNode.getInput(1)
const negativeInput = await ksamplerNode.getInput(2)

const positiveInputPos = await getSlotCenter(
comfyPage.page,
ksamplerNode.id,
1,
true
)

const sourceSlot = await comfyPage.getSubgraphInputSlot()
const calculatedSourcePos = await sourceSlot.getOpenSlotPosition()

await comfyMouse.move(calculatedSourcePos)
await comfyMouse.drag(positiveInputPos)
await comfyMouse.drop()

// Verify connection went to the correct slot
const positiveLinks = await positiveInput.getLinkCount()
const negativeLinks = await negativeInput.getLinkCount()
expect(positiveLinks).toBe(1)
expect(negativeLinks).toBe(0)
})
Comment on lines +997 to +1042
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Subgraph regression test: add sync + avoid “first subgraph” ambiguity

  • After ConvertToSubgraph + enterSubgraph(), add an explicit wait for subgraph Vue nodes (or the KSampler node) before querying getNodeRefsByType('KSampler', true).
  • Use enterSubgraph(String(ksamplerNode.id)) since the helper supports it, to prevent accidental clicks if multiple subgraph-enter buttons exist.
  • Add a quick assertion that ksamplerNode exists before dereferencing .id.
Proposed change
     let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0]
+    expect(ksamplerNode).toBeTruthy()
     await comfyPage.vueNodes.selectNode(String(ksamplerNode.id))
     await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph')

     // Enter the subgraph
-    await comfyPage.vueNodes.enterSubgraph()
+    await comfyPage.vueNodes.enterSubgraph(String(ksamplerNode.id))
     await fitToViewInstant(comfyPage)
+    await comfyPage.vueNodes.waitForNodes()

     // Get the KSampler node inside the subgraph
     ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0]
+    expect(ksamplerNode).toBeTruthy()
🤖 Prompt for AI Agents
In `@browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts`
around lines 997 - 1042, Test flakiness: after calling
comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph') and before querying
the inner KSampler, wait for the subgraph Vue nodes (or the KSampler) to appear,
assert that ksamplerNode is defined, and call comfyPage.vueNodes.enterSubgraph
with the node id (e.g.
comfyPage.vueNodes.enterSubgraph(String(ksamplerNode.id))) instead of the no-arg
enter to avoid ambiguous clicks; then proceed to call
comfyPage.getNodeRefsByType('KSampler', true) and the subsequent slot/position
logic.

})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 2 additions & 7 deletions src/lib/litegraph/src/LGraphNode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties'
import {
calculateInputSlotPos,
calculateInputSlotPosFromSlot,
calculateOutputSlotPos,
getSlotPosition
} from '@/renderer/core/canvas/litegraph/slotCalculations'
import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations'
Expand Down Expand Up @@ -3349,7 +3347,7 @@ export class LGraphNode
* @returns Position of the input slot
*/
getInputPos(slot: number): Point {
return calculateInputSlotPos(this.#getSlotPositionContext(), slot)
return getSlotPosition(this, slot, true)
}
Comment on lines 3349 to 3351
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid ambiguous getSlotPosition symbol resolution (import vs method)

With getInputPos / getOutputPos now delegating to getSlotPosition(...), the class also defines a getSlotPosition(...) method later, which makes call-sites easy to misread. Consider aliasing the import to make it unambiguous.

Proposed change
 import {
   calculateInputSlotPosFromSlot,
-  getSlotPosition
+  getSlotPosition as getSlotPositionFromLayout
 } from '@/renderer/core/canvas/litegraph/slotCalculations'

   getInputPos(slot: number): Point {
-    return getSlotPosition(this, slot, true)
+    return getSlotPositionFromLayout(this, slot, true)
   }

   getOutputPos(outputSlotIndex: number): Point {
-    return getSlotPosition(this, outputSlotIndex, false)
+    return getSlotPositionFromLayout(this, outputSlotIndex, false)
   }

   getSlotPosition(slotIndex: number, isInput: boolean): Point {
-    return getSlotPosition(this, slotIndex, isInput)
+    return getSlotPositionFromLayout(this, slotIndex, isInput)
   }

Also applies to: 3369-3371


/**
Expand All @@ -3369,10 +3367,7 @@ export class LGraphNode
* @returns Position of the output slot
*/
getOutputPos(outputSlotIndex: number): Point {
return calculateOutputSlotPos(
this.#getSlotPositionContext(),
outputSlotIndex
)
return getSlotPosition(this, outputSlotIndex, false)
}

/**
Expand Down
63 changes: 33 additions & 30 deletions src/renderer/core/canvas/litegraph/slotCalculations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface SlotPositionContext {
* @param slot The input slot index
* @returns Position of the input slot center in graph coordinates
*/
export function calculateInputSlotPos(
function calculateInputSlotPos(
context: SlotPositionContext,
slot: number
): Point {
Expand Down Expand Up @@ -93,7 +93,7 @@ export function calculateInputSlotPosFromSlot(
* @param slot The output slot index
* @returns Position of the output slot center in graph coordinates
*/
export function calculateOutputSlotPos(
function calculateOutputSlotPos(
context: SlotPositionContext,
slot: number
): Point {
Expand Down Expand Up @@ -138,38 +138,41 @@ export function getSlotPosition(
slotIndex: number,
isInput: boolean
): Point {
// Try to get precise position from slot layout (DOM-registered)
const slotKey = getSlotKey(String(node.id), slotIndex, isInput)
const slotLayout = layoutStore.getSlotLayout(slotKey)
if (slotLayout) {
return [slotLayout.position.x, slotLayout.position.y]
}

// Fallback: derive position from node layout tree and slot model
const nodeLayout = layoutStore.getNodeLayoutRef(String(node.id)).value

if (nodeLayout) {
// Create context from layout tree data
const context: SlotPositionContext = {
nodeX: nodeLayout.position.x,
nodeY: nodeLayout.position.y,
nodeWidth: nodeLayout.size.width,
nodeHeight: nodeLayout.size.height,
collapsed: node.flags.collapsed || false,
collapsedWidth: node._collapsed_width,
slotStartY: node.constructor.slot_start_y,
inputs: node.inputs,
outputs: node.outputs,
widgets: node.widgets
// Only use DOM-registered slot positions when Vue nodes mode is enabled
if (LiteGraph.vueNodesMode) {
// Try to get precise position from slot layout (DOM-registered)
const slotKey = getSlotKey(String(node.id), slotIndex, isInput)
const slotLayout = layoutStore.getSlotLayout(slotKey)
if (slotLayout) {
return [slotLayout.position.x, slotLayout.position.y]
}

// Use helper to calculate position
return isInput
? calculateInputSlotPos(context, slotIndex)
: calculateOutputSlotPos(context, slotIndex)
// Fallback: derive position from node layout tree and slot model
const nodeLayout = layoutStore.getNodeLayoutRef(String(node.id)).value

if (nodeLayout) {
// Create context from layout tree data
const context: SlotPositionContext = {
nodeX: nodeLayout.position.x,
nodeY: nodeLayout.position.y,
nodeWidth: nodeLayout.size.width,
nodeHeight: nodeLayout.size.height,
collapsed: node.flags.collapsed || false,
collapsedWidth: node._collapsed_width,
slotStartY: node.constructor.slot_start_y,
inputs: node.inputs,
outputs: node.outputs,
widgets: node.widgets
}

// Use helper to calculate position
return isInput
? calculateInputSlotPos(context, slotIndex)
: calculateOutputSlotPos(context, slotIndex)
}
}

// Fallback: calculate directly from node properties if layout not available
// Fallback: calculate directly from node properties (legacy litegraph behavior)
const context: SlotPositionContext = {
nodeX: node.pos[0],
nodeY: node.pos[1],
Expand Down