refactor: replace synthetic events and page.evaluate with real Playwright interactions

- Replace synthetic CanvasPointerEvent dispatch with real page.mouse clicks
- Split interactWithSubgraphSlot into getSlotScreenPositions + rightClickSlot + doubleClickSlot
- Replace isInSubgraph evaluate with breadcrumb visibility check
- Remove silent evaluate fallback from exitViaBreadcrumb
- Replace 3 evaluateAll/evaluate/waitForFunction in VueNodeHelpers with locator API
- Replace brittle CSS selectors with data-testid locators
- Replace getNodeCount and findSubgraphNodeId with DOM-based approaches
- Add data-testid to SubgraphBreadcrumbItem.vue
- Remove debug console.warn from NodeSlotReference

Amp-Thread-ID: https://ampcode.com/threads/T-019d32eb-d58e-734b-b3f1-3036334fa774
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-03-28 13:55:57 -07:00
parent d2358c83e8
commit a13c333cec
8 changed files with 183 additions and 216 deletions

View File

@@ -2,6 +2,7 @@
* Vue Node Test Helpers
*/
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { TestIds } from './selectors'
import { VueNodeFixture } from './utils/vueNodeFixtures'
@@ -55,11 +56,11 @@ export class VueNodeHelpers {
* Get all Vue node IDs currently in the DOM
*/
async getNodeIds(): Promise<string[]> {
return await this.nodes.evaluateAll((nodes) =>
nodes
.map((n) => n.getAttribute('data-node-id'))
.filter((id): id is string => id !== null)
const allNodes = await this.nodes.all()
const ids = await Promise.all(
allNodes.map((l) => l.getAttribute('data-node-id'))
)
return ids.filter((id): id is string => id !== null)
}
/**
@@ -67,7 +68,7 @@ export class VueNodeHelpers {
*/
async selectNode(nodeId: string): Promise<void> {
await this.page
.locator(`[data-node-id="${nodeId}"] .lg-node-header`)
.locator(`[data-node-id="${nodeId}"] [data-testid^="node-header-"]`)
.click()
}
@@ -83,7 +84,7 @@ export class VueNodeHelpers {
// Add additional nodes with Ctrl+click on header
for (let i = 1; i < nodeIds.length; i++) {
await this.page
.locator(`[data-node-id="${nodeIds[i]}"] .lg-node-header`)
.locator(`[data-node-id="${nodeIds[i]}"] [data-testid^="node-header-"]`)
.click({
modifiers: ['Control']
})
@@ -121,7 +122,7 @@ export class VueNodeHelpers {
const node = this.getNodeByTitle(title).first()
await node.waitFor({ state: 'visible' })
const nodeId = await node.evaluate((el) => el.getAttribute('data-node-id'))
const nodeId = await node.getAttribute('data-node-id')
if (!nodeId) {
throw new Error(
`Vue node titled "${title}" is missing its data-node-id attribute`
@@ -136,12 +137,12 @@ export class VueNodeHelpers {
*/
async waitForNodes(expectedCount?: number): Promise<void> {
if (expectedCount !== undefined) {
await this.page.waitForFunction(
(count) => document.querySelectorAll('[data-node-id]').length >= count,
expectedCount
await expect(this.page.locator('[data-node-id]')).toHaveCount(
expectedCount,
{ timeout: 30_000 }
)
} else {
await this.page.waitForSelector('[data-node-id]')
await this.page.locator('[data-node-id]').first().waitFor()
}
}

View File

@@ -1,10 +1,7 @@
import { expect } from '@playwright/test'
import type { ConsoleMessage, Locator, Page } from '@playwright/test'
import type {
CanvasPointerEvent,
Subgraph
} from '@/lib/litegraph/src/litegraph'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyPage } from '../ComfyPage'
@@ -19,26 +16,17 @@ export class SubgraphHelper {
return this.comfyPage.page
}
/**
* Core helper method for interacting with subgraph I/O slots.
* Handles both input/output slots and both right-click/double-click actions.
*
* @param slotType - 'input' or 'output'
* @param action - 'rightClick' or 'doubleClick'
* @param slotName - Optional specific slot name to target
*/
private async interactWithSubgraphSlot(
private async getSlotScreenPositions(
slotType: 'input' | 'output',
action: 'rightClick' | 'doubleClick',
slotName?: string
): Promise<void> {
const foundSlot = await this.page.evaluate(
async (params) => {
): Promise<{ x: number; y: number; slotName: string }[]> {
return this.page.evaluate(
(params) => {
const { slotType, action, targetSlotName } = params
const app = window.app!
const currentGraph = app.canvas!.graph!
// Check if we're in a subgraph
if (!('inputNode' in currentGraph)) {
throw new Error(
'Not in a subgraph - this method only works inside subgraphs'
@@ -47,7 +35,6 @@ export class SubgraphHelper {
const subgraph = currentGraph as Subgraph
// Get the appropriate node and slots
const node =
slotType === 'input' ? subgraph.inputNode : subgraph.outputNode
const slots = slotType === 'input' ? subgraph.inputs : subgraph.outputs
@@ -60,12 +47,11 @@ export class SubgraphHelper {
throw new Error(`No ${slotType} slots found in subgraph`)
}
// Filter slots based on target name and action type
const slotsToTry = targetSlotName
? slots.filter((slot) => slot.name === targetSlotName)
: action === 'rightClick'
? slots
: [slots[0]] // Right-click tries all, double-click uses first
: [slots[0]]
if (slotsToTry.length === 0) {
throw new Error(
@@ -75,95 +61,98 @@ export class SubgraphHelper {
)
}
// Handle the interaction based on action type
if (action === 'rightClick') {
// Right-click: try each slot until one works
for (const slot of slotsToTry) {
const results: { x: number; y: number; slotName: string }[] = []
for (const slot of slotsToTry) {
let canvasX: number
let canvasY: number
if (action === 'rightClick') {
if (!slot.pos) continue
const event = {
canvasX: slot.pos[0],
canvasY: slot.pos[1],
button: 2, // Right mouse button
preventDefault: () => {},
stopPropagation: () => {}
canvasX = slot.pos[0]
canvasY = slot.pos[1]
} else {
if (!slot.boundingRect) {
throw new Error(`${slotType} slot bounding rect not found`)
}
if (node.onPointerDown) {
node.onPointerDown(
event as Partial<CanvasPointerEvent> as CanvasPointerEvent,
app.canvas.pointer,
app.canvas.linkConnector
)
return {
success: true,
slotName: slot.name,
x: slot.pos[0],
y: slot.pos[1]
}
}
}
} else if (action === 'doubleClick') {
// Double-click: use first slot with bounding rect center
const slot = slotsToTry[0]
if (!slot.boundingRect) {
throw new Error(`${slotType} slot bounding rect not found`)
const rect = slot.boundingRect
canvasX = rect[0] + rect[2] / 2
canvasY = rect[1] + rect[3] / 2
}
const rect = slot.boundingRect
const testX = rect[0] + rect[2] / 2 // x + width/2
const testY = rect[1] + rect[3] / 2 // y + height/2
const event = {
canvasX: testX,
canvasY: testY,
button: 0, // Left mouse button
preventDefault: () => {},
stopPropagation: () => {}
}
if (node.onPointerDown) {
node.onPointerDown(
event as Partial<CanvasPointerEvent> as CanvasPointerEvent,
app.canvas.pointer,
app.canvas.linkConnector
)
// Trigger double-click
if (app.canvas.pointer.onDoubleClick) {
app.canvas.pointer.onDoubleClick(
event as Partial<CanvasPointerEvent> as CanvasPointerEvent
)
}
}
return { success: true, slotName: slot.name, x: testX, y: testY }
const [clientX, clientY] = app.canvasPosToClientPos([
canvasX,
canvasY
])
results.push({ x: clientX, y: clientY, slotName: slot.name })
}
return { success: false }
return results
},
{ slotType, action, targetSlotName: slotName }
)
}
if (!foundSlot.success) {
const actionText =
action === 'rightClick' ? 'open context menu for' : 'double-click'
private async rightClickSlot(
slotType: 'input' | 'output',
slotName?: string
): Promise<void> {
const positions = await this.getSlotScreenPositions(
slotType,
'rightClick',
slotName
)
if (positions.length === 0) {
throw new Error(
slotName
? `Could not ${actionText} ${slotType} slot '${slotName}'`
: `Could not find any ${slotType} slot to ${actionText}`
? `Could not open context menu for ${slotType} slot '${slotName}'`
: `Could not find any ${slotType} slot to open context menu for`
)
}
// Wait for the appropriate UI element to appear
if (action === 'rightClick') {
await this.page.waitForSelector('.litemenu-entry', {
state: 'visible',
timeout: 5000
})
} else {
for (const pos of positions) {
await this.page.mouse.click(pos.x, pos.y, { button: 'right' })
await this.comfyPage.nextFrame()
const menuVisible = await this.page
.waitForSelector('.litemenu-entry', {
state: 'visible',
timeout: 1000
})
.then(() => true)
.catch(() => false)
if (menuVisible) return
}
await this.page.waitForSelector('.litemenu-entry', {
state: 'visible',
timeout: 5000
})
}
private async doubleClickSlot(
slotType: 'input' | 'output',
slotName?: string
): Promise<void> {
const positions = await this.getSlotScreenPositions(
slotType,
'doubleClick',
slotName
)
if (positions.length === 0) {
throw new Error(
slotName
? `Could not double-click ${slotType} slot '${slotName}'`
: `Could not find any ${slotType} slot to double-click`
)
}
const pos = positions[0]
await this.page.mouse.dblclick(pos.x, pos.y)
await this.comfyPage.nextFrame()
}
/**
@@ -180,7 +169,7 @@ export class SubgraphHelper {
* @returns Promise that resolves when the context menu appears
*/
async rightClickInputSlot(inputName?: string): Promise<void> {
return this.interactWithSubgraphSlot('input', 'rightClick', inputName)
return this.rightClickSlot('input', inputName)
}
/**
@@ -194,7 +183,7 @@ export class SubgraphHelper {
* @returns Promise that resolves when the context menu appears
*/
async rightClickOutputSlot(outputName?: string): Promise<void> {
return this.interactWithSubgraphSlot('output', 'rightClick', outputName)
return this.rightClickSlot('output', outputName)
}
/**
@@ -206,7 +195,7 @@ export class SubgraphHelper {
* @returns Promise that resolves when the rename dialog appears
*/
async doubleClickInputSlot(inputName?: string): Promise<void> {
return this.interactWithSubgraphSlot('input', 'doubleClick', inputName)
return this.doubleClickSlot('input', inputName)
}
/**
@@ -218,7 +207,7 @@ export class SubgraphHelper {
* @returns Promise that resolves when the rename dialog appears
*/
async doubleClickOutputSlot(outputName?: string): Promise<void> {
return this.interactWithSubgraphSlot('output', 'doubleClick', outputName)
return this.doubleClickSlot('output', outputName)
}
/**
@@ -327,26 +316,14 @@ export class SubgraphHelper {
}
async isInSubgraph(): Promise<boolean> {
return this.page.evaluate(() => {
const graph = window.app!.canvas.graph
return !!graph && 'inputNode' in graph
})
const breadcrumb = this.page.getByTestId(TestIds.breadcrumb.subgraph)
return breadcrumb.isVisible()
}
async exitViaBreadcrumb(): Promise<void> {
const breadcrumb = this.page.getByTestId(TestIds.breadcrumb.subgraph)
const parentLink = breadcrumb.getByRole('link').first()
if (await parentLink.isVisible()) {
await parentLink.click()
} else {
await this.page.evaluate(() => {
const canvas = window.app!.canvas
const graph = canvas.graph
if (!graph) return
canvas.setGraph(graph.rootGraph)
})
}
await parentLink.click()
await this.comfyPage.nextFrame()
await expect.poll(async () => this.isInSubgraph()).toBe(false)
}
@@ -408,11 +385,8 @@ export class SubgraphHelper {
})
}
/** Reads from `window.app.canvas.graph` (viewed root or nested subgraph). */
async getNodeCount(): Promise<number> {
return this.page.evaluate(() => {
return window.app!.canvas.graph!.nodes?.length || 0
})
return this.page.locator('[data-node-id]').count()
}
async getSlotCount(type: 'input' | 'output'): Promise<number> {
@@ -449,13 +423,13 @@ export class SubgraphHelper {
}
async findSubgraphNodeId(): Promise<string> {
const id = await this.page.evaluate(() => {
const graph = window.app!.canvas.graph!
const node = graph.nodes.find(
(n) => typeof n.isSubgraphNode === 'function' && n.isSubgraphNode()
)
return node ? String(node.id) : null
})
const enterButton = this.page
.getByTestId(TestIds.widgets.subgraphEnterButton)
.first()
const nodeEl = enterButton
.locator('xpath=ancestor::*[@data-node-id]')
.first()
const id = await nodeEl.getAttribute('data-node-id')
if (!id) throw new Error('No subgraph node found in current graph')
return id
}

View File

@@ -83,7 +83,9 @@ export const TestIds = {
opensAs: 'builder-opens-as'
},
breadcrumb: {
subgraph: 'subgraph-breadcrumb'
subgraph: 'subgraph-breadcrumb',
item: 'subgraph-breadcrumb-item',
root: 'subgraph-breadcrumb-root'
},
templates: {
content: 'template-workflows-content',

View File

@@ -120,19 +120,6 @@ class NodeSlotReference {
const convertedPos =
window.app!.canvas.ds!.convertOffsetToCanvas(rawPos)
// Debug logging - convert Float64Arrays to regular arrays for visibility
console.warn(
`NodeSlotReference debug for ${type} slot ${index} on node ${id}:`,
{
nodePos: [node.pos[0], node.pos[1]],
nodeSize: [node.size[0], node.size[1]],
rawConnectionPos: [rawPos[0], rawPos[1]],
convertedPos: [convertedPos[0], convertedPos[1]],
currentGraphType:
'inputNode' in window.app!.canvas.graph! ? 'Subgraph' : 'LGraph'
}
)
return convertedPos
},
[this.type, this.node.id, this.index] as const
@@ -490,12 +477,7 @@ export class NodeReference {
y: nodePos.y - titleHeight / 2
}
const checkIsInSubgraph = async () => {
return this.comfyPage.page.evaluate(() => {
const graph = window.app!.canvas.graph
return !!graph && 'inputNode' in graph
})
}
const checkIsInSubgraph = () => this.comfyPage.subgraph.isInSubgraph()
await expect(async () => {
// Try just clicking the enter button first

View File

@@ -11,10 +11,7 @@ const TEST_WIDGET_CONTENT = 'Test content that should persist'
// Common selectors
const SELECTORS = {
breadcrumb: '.subgraph-breadcrumb',
promptDialog: '.graphdialog input',
nodeSearchContainer: '.node-search-container',
domWidget: '.comfy-multiline-input'
promptDialog: '.graphdialog input'
} as const
test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
@@ -447,16 +444,13 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
const initialNodeCount = await comfyPage.subgraph.getNodeCount()
const nodesInSubgraph = await comfyPage.page.evaluate(() => {
const nodes = window.app!.canvas.graph!.nodes
return nodes?.[0]?.id || null
})
const firstNodeLocator = comfyPage.page.locator('[data-node-id]').first()
await expect(firstNodeLocator).toBeVisible()
const firstNodeId = await firstNodeLocator.getAttribute('data-node-id')
expect(nodesInSubgraph).not.toBeNull()
expect(firstNodeId).not.toBeNull()
const nodeToClone = await comfyPage.nodeOps.getNodeRefById(
String(nodesInSubgraph)
)
const nodeToClone = await comfyPage.nodeOps.getNodeRefById(firstNodeId!)
await nodeToClone.click('title')
await comfyPage.nextFrame()
@@ -518,12 +512,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Navigate into subgraph
await subgraphNode.navigateIntoSubgraph()
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb, {
state: 'visible',
timeout: 20000
})
const breadcrumb = comfyPage.page.locator(SELECTORS.breadcrumb)
const breadcrumb = comfyPage.page.getByTestId(TestIds.breadcrumb.subgraph)
await breadcrumb.waitFor({ state: 'visible', timeout: 20000 })
const initialBreadcrumbText = await breadcrumb.textContent()
// Go back and edit title
@@ -548,7 +538,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Navigate back into subgraph
await subgraphNode.navigateIntoSubgraph()
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
await breadcrumb.waitFor({ state: 'visible' })
const updatedBreadcrumbText = await breadcrumb.textContent()
expect(updatedBreadcrumbText).toContain(UPDATED_SUBGRAPH_TITLE)
@@ -566,7 +556,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
await expect(comfyPage.page.locator(SELECTORS.breadcrumb)).toBeVisible()
await expect(
comfyPage.page.getByTestId(TestIds.breadcrumb.subgraph)
).toBeVisible()
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.nextFrame()
@@ -611,7 +603,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
// Verify promoted widget is visible in parent graph
const parentTextarea = comfyPage.page.locator(SELECTORS.domWidget)
const parentTextarea = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
await expect(parentTextarea).toBeVisible()
await expect(parentTextarea).toHaveCount(1)
@@ -621,7 +615,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await subgraphNode.navigateIntoSubgraph()
// Verify widget is visible in subgraph
const subgraphTextarea = comfyPage.page.locator(SELECTORS.domWidget)
const subgraphTextarea = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
await expect(subgraphTextarea).toBeVisible()
await expect(subgraphTextarea).toHaveCount(1)
@@ -630,7 +626,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
// Verify widget is still visible
const backToParentTextarea = comfyPage.page.locator(SELECTORS.domWidget)
const backToParentTextarea = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
await expect(backToParentTextarea).toBeVisible()
await expect(backToParentTextarea).toHaveCount(1)
})
@@ -642,19 +640,25 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
'subgraphs/subgraph-with-promoted-text-widget'
)
const textarea = comfyPage.page.locator(SELECTORS.domWidget)
const textarea = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
await textarea.fill(TEST_WIDGET_CONTENT)
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
await subgraphNode.navigateIntoSubgraph()
const subgraphTextarea = comfyPage.page.locator(SELECTORS.domWidget)
const subgraphTextarea = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
await expect(subgraphTextarea).toHaveValue(TEST_WIDGET_CONTENT)
await comfyPage.page.keyboard.press('Escape')
await comfyPage.nextFrame()
const parentTextarea = comfyPage.page.locator(SELECTORS.domWidget)
const parentTextarea = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
await expect(parentTextarea).toHaveValue(TEST_WIDGET_CONTENT)
})
@@ -665,18 +669,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
'subgraphs/subgraph-with-promoted-text-widget'
)
const initialCount = await comfyPage.page
.locator(SELECTORS.domWidget)
.count()
const domWidget = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
const initialCount = await domWidget.count()
expect(initialCount).toBe(1)
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
await subgraphNode.delete()
const finalCount = await comfyPage.page
.locator(SELECTORS.domWidget)
.count()
const finalCount = await domWidget.count()
expect(finalCount).toBe(0)
})
@@ -690,7 +693,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.workflow.loadWorkflow(workflowName)
const textareaCount = await comfyPage.page
.locator(SELECTORS.domWidget)
.getByTestId(TestIds.widgets.domWidgetTextarea)
.count()
expect(textareaCount).toBe(1)
@@ -702,10 +705,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.subgraph.removeSlot('input', 'text')
// Wait for breadcrumb to be visible
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb, {
state: 'visible',
timeout: 5000
})
await comfyPage.page
.getByTestId(TestIds.breadcrumb.subgraph)
.waitFor({ state: 'visible', timeout: 5000 })
// Click breadcrumb to navigate back to parent graph
const homeBreadcrumb = comfyPage.page.locator(
@@ -730,25 +732,22 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
'subgraphs/subgraph-with-multiple-promoted-widgets'
)
const parentCount = await comfyPage.page
.locator(SELECTORS.domWidget)
.count()
const domWidget = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
const parentCount = await domWidget.count()
expect(parentCount).toBeGreaterThan(1)
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
await subgraphNode.navigateIntoSubgraph()
const subgraphCount = await comfyPage.page
.locator(SELECTORS.domWidget)
.count()
const subgraphCount = await domWidget.count()
expect(subgraphCount).toBe(parentCount)
await comfyPage.page.keyboard.press('Escape')
await comfyPage.nextFrame()
const finalCount = await comfyPage.page
.locator(SELECTORS.domWidget)
.count()
const finalCount = await domWidget.count()
expect(finalCount).toBe(parentCount)
})
})
@@ -796,7 +795,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Navigate into subgraph
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
await comfyPage.page
.getByTestId(TestIds.breadcrumb.subgraph)
.waitFor({ state: 'visible' })
// Verify we're in a subgraph
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
@@ -822,7 +823,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
await comfyPage.page
.getByTestId(TestIds.breadcrumb.subgraph)
.waitFor({ state: 'visible' })
// Verify we're in a subgraph
if (!(await comfyPage.subgraph.isInSubgraph())) {

View File

@@ -310,10 +310,9 @@ test.describe(
})
await comfyPage.nextFrame()
const firstNodeExists = await comfyPage.page.evaluate(() => {
return !!window.app!.graph!.getNodeById('7')
})
expect(firstNodeExists).toBe(false)
await expect(comfyPage.page.locator('[data-node-id="7"]')).toHaveCount(
0
)
const secondNodeAfter = await getPseudoPreviewWidgets(comfyPage, '8')
expect(secondNodeAfter).toEqual(secondNodeBefore)

View File

@@ -146,7 +146,7 @@ test.describe(
await expect(nodeBody).toBeVisible()
// Widgets section should exist and have at least one widget
const widgets = nodeBody.locator('.lg-node-widgets > div')
const widgets = nodeBody.getByTestId(TestIds.widgets.widget)
await expect(widgets.first()).toBeVisible()
})
@@ -176,7 +176,7 @@ test.describe(
await expect(subgraphVueNode).toBeVisible()
const nodeBody = subgraphVueNode.locator('[data-testid="node-body-11"]')
const widgets = nodeBody.locator('.lg-node-widgets > div')
const widgets = nodeBody.getByTestId(TestIds.widgets.widget)
const count = await widgets.count()
expect(count).toBeGreaterThan(1)
})
@@ -559,15 +559,16 @@ test.describe(
await comfyPage.page.keyboard.up('Alt')
await comfyPage.nextFrame()
const subgraphNodeIds = await comfyPage.page.evaluate(() => {
const graph = window.app!.canvas.graph!
return graph.nodes
.filter(
(n) =>
typeof n.isSubgraphNode === 'function' && n.isSubgraphNode()
)
.map((n) => String(n.id))
})
const enterButtons = comfyPage.page.getByTestId(
TestIds.widgets.subgraphEnterButton
)
const allButtons = await enterButtons.all()
const subgraphNodeIds: string[] = []
for (const btn of allButtons) {
const nodeEl = btn.locator('xpath=ancestor::*[@data-node-id]').first()
const id = await nodeEl.getAttribute('data-node-id')
if (id) subgraphNodeIds.push(id)
}
expect(subgraphNodeIds.length).toBeGreaterThan(1)
for (const nodeId of subgraphNodeIds) {

View File

@@ -7,6 +7,11 @@
}"
draggable="false"
class="p-breadcrumb-item-link h-8 cursor-pointer px-2"
:data-testid="
item.key === 'root'
? 'subgraph-breadcrumb-root'
: 'subgraph-breadcrumb-item'
"
:class="{
'flex items-center gap-1': isActive,
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,