mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-13 11:11:00 +00:00
Compare commits
8 Commits
codex/clou
...
drjkl/test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ece53af7aa | ||
|
|
6a1dfe518d | ||
|
|
8074c404e9 | ||
|
|
22b4e3b1a6 | ||
|
|
f5c0da885a | ||
|
|
591b3d2306 | ||
|
|
1004547939 | ||
|
|
a13c333cec |
@@ -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'
|
||||
@@ -59,11 +60,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((node) => node.getAttribute('data-node-id'))
|
||||
)
|
||||
return ids.filter((id): id is string => id !== null)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +72,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()
|
||||
}
|
||||
|
||||
@@ -87,7 +88,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']
|
||||
})
|
||||
@@ -125,7 +126,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`
|
||||
@@ -140,12 +141,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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
coordinateSource: 'pos' | 'boundingRect',
|
||||
slotName?: string
|
||||
): Promise<void> {
|
||||
const foundSlot = await this.page.evaluate(
|
||||
async (params) => {
|
||||
const { slotType, action, targetSlotName } = params
|
||||
): Promise<{ x: number; y: number; slotName: string }[]> {
|
||||
return this.page.evaluate(
|
||||
(params) => {
|
||||
const { slotType, coordinateSource, 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'
|
||||
: coordinateSource === 'pos'
|
||||
? slots
|
||||
: [slots[0]] // Right-click tries all, double-click uses first
|
||||
: [slots[0]]
|
||||
|
||||
if (slotsToTry.length === 0) {
|
||||
throw new Error(
|
||||
@@ -75,95 +61,102 @@ 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 offsetX: number
|
||||
let offsetY: number
|
||||
|
||||
if (coordinateSource === 'pos') {
|
||||
if (!slot.pos) continue
|
||||
|
||||
const event = {
|
||||
canvasX: slot.pos[0],
|
||||
canvasY: slot.pos[1],
|
||||
button: 2, // Right mouse button
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {}
|
||||
offsetX = slot.pos[0]
|
||||
offsetY = 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
|
||||
offsetX = rect[0] + rect[2] / 2
|
||||
offsetY = 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 [canvasX, canvasY] = app.canvas.ds.convertOffsetToCanvas([
|
||||
offsetX,
|
||||
offsetY
|
||||
])
|
||||
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 }
|
||||
{ slotType, coordinateSource, targetSlotName: slotName }
|
||||
)
|
||||
}
|
||||
|
||||
private async rightClickSlot(
|
||||
slotType: 'input' | 'output',
|
||||
slotName?: string
|
||||
): Promise<void> {
|
||||
const positions = await this.getSlotScreenPositions(
|
||||
slotType,
|
||||
'pos',
|
||||
slotName
|
||||
)
|
||||
|
||||
if (!foundSlot.success) {
|
||||
const actionText =
|
||||
action === 'rightClick' ? 'open context menu for' : 'double-click'
|
||||
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 {
|
||||
const menuEntry = this.page.locator('.litemenu-entry').first()
|
||||
|
||||
for (const pos of positions) {
|
||||
await this.page.mouse.click(pos.x, pos.y, { button: 'right' })
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
const menuVisible = await menuEntry
|
||||
.waitFor({ state: 'visible', timeout: 1000 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (menuVisible) return
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
slotName
|
||||
? `Context menu did not appear after right-clicking ${slotType} slot '${slotName}'`
|
||||
: `Context menu did not appear after right-clicking all ${slotType} slots`
|
||||
)
|
||||
}
|
||||
|
||||
private async doubleClickSlot(
|
||||
slotType: 'input' | 'output',
|
||||
slotName?: string
|
||||
): Promise<void> {
|
||||
const positions = await this.getSlotScreenPositions(
|
||||
slotType,
|
||||
'boundingRect',
|
||||
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 +173,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 +187,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 +199,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 +211,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,20 +326,24 @@ export class SubgraphHelper {
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
get breadcrumb(): Locator {
|
||||
return this.page.getByTestId(TestIds.breadcrumb.subgraph)
|
||||
}
|
||||
|
||||
async exitSubgraph(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
const canvas = window.app!.canvas
|
||||
const graph = canvas.graph
|
||||
if (!graph) return
|
||||
canvas.setGraph(graph.rootGraph)
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
await expect.poll(async () => this.isInSubgraph()).toBe(false)
|
||||
}
|
||||
|
||||
async exitViaBreadcrumb(): Promise<void> {
|
||||
const rootItem = this.breadcrumb.getByTestId(TestIds.breadcrumb.root)
|
||||
await rootItem.click()
|
||||
await this.comfyPage.nextFrame()
|
||||
await expect.poll(async () => this.isInSubgraph()).toBe(false)
|
||||
}
|
||||
@@ -408,8 +405,12 @@ export class SubgraphHelper {
|
||||
})
|
||||
}
|
||||
|
||||
/** Reads from `window.app.canvas.graph` (viewed root or nested subgraph). */
|
||||
async getNodeCount(): Promise<number> {
|
||||
// Prefer DOM count (Vue nodes only render for the active graph).
|
||||
// Falls back to graph model for canvas-only mode or empty graphs (0 DOM nodes).
|
||||
const domCount = await this.page.locator('[data-node-id]').count()
|
||||
if (domCount > 0) return domCount
|
||||
|
||||
return this.page.evaluate(() => {
|
||||
return window.app!.canvas.graph!.nodes?.length || 0
|
||||
})
|
||||
@@ -449,6 +450,18 @@ export class SubgraphHelper {
|
||||
}
|
||||
|
||||
async findSubgraphNodeId(): Promise<string> {
|
||||
const enterButton = this.page
|
||||
.getByTestId(TestIds.widgets.subgraphEnterButton)
|
||||
.first()
|
||||
|
||||
if ((await enterButton.count()) > 0) {
|
||||
const nodeEl = enterButton
|
||||
.locator('xpath=ancestor::*[@data-node-id]')
|
||||
.first()
|
||||
const id = await nodeEl.getAttribute('data-node-id')
|
||||
if (id) return id
|
||||
}
|
||||
|
||||
const id = await this.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
const node = graph.nodes.find(
|
||||
@@ -471,13 +484,11 @@ export class SubgraphHelper {
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
async convertDefaultKSamplerToSubgraph(): Promise<NodeReference> {
|
||||
async loadDefaultAndConvertKSamplerToSubgraph(): Promise<NodeReference> {
|
||||
await this.comfyPage.workflow.loadWorkflow('default')
|
||||
const ksampler = await this.comfyPage.nodeOps.getNodeRefById('3')
|
||||
await ksampler.click('title')
|
||||
const subgraphNode = await ksampler.convertToSubgraph()
|
||||
await this.comfyPage.nextFrame()
|
||||
return subgraphNode
|
||||
return ksampler.convertToSubgraph()
|
||||
}
|
||||
|
||||
async packAllInteriorNodes(hostNodeId: string): Promise<void> {
|
||||
@@ -492,7 +503,7 @@ export class SubgraphHelper {
|
||||
canvas.graph!.convertToSubgraph(canvas.selectedItems)
|
||||
})
|
||||
await this.comfyPage.nextFrame()
|
||||
await this.exitViaBreadcrumb()
|
||||
await this.exitSubgraph()
|
||||
await this.comfyPage.canvas.click()
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
@@ -87,7 +87,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',
|
||||
|
||||
@@ -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
|
||||
@@ -393,7 +380,18 @@ export class NodeReference {
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
async delete(): Promise<void> {
|
||||
await this.click('title')
|
||||
const header = this.comfyPage.page
|
||||
.locator(`[data-node-id="${this.id}"] [data-testid^="node-header-"]`)
|
||||
.first()
|
||||
|
||||
if ((await header.count()) > 0) {
|
||||
await header.dispatchEvent('pointerdown', { bubbles: true })
|
||||
await header.dispatchEvent('pointerup', { bubbles: true })
|
||||
await header.dispatchEvent('click', { bubbles: true })
|
||||
} else {
|
||||
await this.click('title')
|
||||
}
|
||||
|
||||
await this.comfyPage.page.keyboard.press('Delete')
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
@@ -490,12 +488,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
|
||||
|
||||
@@ -76,7 +76,7 @@ test.describe(
|
||||
await comfyPage.page.waitForSelector(dialog, { state: 'hidden' })
|
||||
|
||||
// Navigate back to parent graph
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
// Verify slot position is still at the widget row after rename
|
||||
const after = await SubgraphHelper.getTextSlotPosition(
|
||||
|
||||
@@ -16,7 +16,7 @@ test.describe(
|
||||
comfyPage
|
||||
}) => {
|
||||
const subgraphNode =
|
||||
await comfyPage.subgraph.convertDefaultKSamplerToSubgraph()
|
||||
await comfyPage.subgraph.loadDefaultAndConvertKSamplerToSubgraph()
|
||||
|
||||
// Enable Vue nodes now that the subgraph has been created
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
|
||||
@@ -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'] }, () => {
|
||||
@@ -110,6 +107,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialInputLabel = await comfyPage.subgraph.getSlotLabel('input')
|
||||
expect(initialInputLabel).not.toBeNull()
|
||||
|
||||
await comfyPage.subgraph.rightClickInputSlot(initialInputLabel!)
|
||||
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
|
||||
@@ -138,6 +136,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialInputLabel = await comfyPage.subgraph.getSlotLabel('input')
|
||||
expect(initialInputLabel).not.toBeNull()
|
||||
|
||||
await comfyPage.subgraph.doubleClickInputSlot(initialInputLabel!)
|
||||
|
||||
@@ -164,6 +163,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialOutputLabel = await comfyPage.subgraph.getSlotLabel('output')
|
||||
expect(initialOutputLabel).not.toBeNull()
|
||||
|
||||
await comfyPage.subgraph.doubleClickOutputSlot(initialOutputLabel!)
|
||||
|
||||
@@ -193,6 +193,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
const initialInputLabel = await comfyPage.subgraph.getSlotLabel('input')
|
||||
expect(initialInputLabel).not.toBeNull()
|
||||
|
||||
// Test that right-click still works for renaming
|
||||
await comfyPage.subgraph.rightClickInputSlot(initialInputLabel!)
|
||||
@@ -447,16 +448,13 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
|
||||
const initialNodeCount = await comfyPage.subgraph.getNodeCount()
|
||||
|
||||
const nodesInSubgraph = await comfyPage.page.evaluate(() => {
|
||||
const firstNodeId = await comfyPage.page.evaluate(() => {
|
||||
const nodes = window.app!.canvas.graph!.nodes
|
||||
return nodes?.[0]?.id || null
|
||||
return nodes?.[0]?.id != null ? String(nodes[0].id) : null
|
||||
})
|
||||
expect(firstNodeId).not.toBeNull()
|
||||
|
||||
expect(nodesInSubgraph).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 +516,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.subgraph.breadcrumb
|
||||
await breadcrumb.waitFor({ state: 'visible', timeout: 20000 })
|
||||
const initialBreadcrumbText = await breadcrumb.textContent()
|
||||
|
||||
// Go back and edit title
|
||||
@@ -548,7 +542,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 +560,7 @@ 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.subgraph.breadcrumb).toBeVisible()
|
||||
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await comfyPage.nextFrame()
|
||||
@@ -584,9 +578,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const breadcrumb = comfyPage.page
|
||||
.getByTestId(TestIds.breadcrumb.subgraph)
|
||||
.locator('.p-breadcrumb')
|
||||
const breadcrumb = comfyPage.subgraph.breadcrumb.locator('.p-breadcrumb')
|
||||
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
@@ -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,14 +705,14 @@ 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, {
|
||||
await comfyPage.subgraph.breadcrumb.waitFor({
|
||||
state: 'visible',
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
// Click breadcrumb to navigate back to parent graph
|
||||
const homeBreadcrumb = comfyPage.page.locator(
|
||||
'.p-breadcrumb-list > :first-child'
|
||||
const homeBreadcrumb = comfyPage.subgraph.breadcrumb.getByTestId(
|
||||
TestIds.breadcrumb.root
|
||||
)
|
||||
await homeBreadcrumb.waitFor({ state: 'visible' })
|
||||
await homeBreadcrumb.click()
|
||||
@@ -730,25 +733,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 +796,7 @@ 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.subgraph.breadcrumb.waitFor({ state: 'visible' })
|
||||
|
||||
// Verify we're in a subgraph
|
||||
expect(await comfyPage.subgraph.isInSubgraph()).toBe(true)
|
||||
@@ -822,7 +822,7 @@ 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.subgraph.breadcrumb.waitFor({ state: 'visible' })
|
||||
|
||||
// Verify we're in a subgraph
|
||||
if (!(await comfyPage.subgraph.isInSubgraph())) {
|
||||
|
||||
@@ -76,7 +76,7 @@ test.describe(
|
||||
await page.waitForSelector(dialog, { state: 'hidden' })
|
||||
|
||||
// 5. Navigate back to parent graph and re-enable Vue nodes
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
|
||||
@@ -31,11 +31,17 @@ test.describe(
|
||||
test('Loads without console warnings about failed widget resolution', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { warnings } = SubgraphHelper.collectConsoleWarnings(comfyPage.page)
|
||||
const { warnings, dispose } = SubgraphHelper.collectConsoleWarnings(
|
||||
comfyPage.page
|
||||
)
|
||||
|
||||
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
||||
try {
|
||||
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
||||
|
||||
expect(warnings).toEqual([])
|
||||
expect(warnings).toEqual([])
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
|
||||
test('Promoted widget renders with normalized name, not legacy prefix', async ({
|
||||
|
||||
@@ -149,7 +149,7 @@ test.describe(
|
||||
const clipNode = await comfyPage.nodeOps.getNodeRefById('10')
|
||||
await clipNode.delete()
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -189,7 +189,7 @@ test.describe(
|
||||
const clipNode = await comfyPage.nodeOps.getNodeRefById('10')
|
||||
await clipNode.delete()
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.widgets.domWidgetTextarea)
|
||||
@@ -310,10 +310,10 @@ test.describe(
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const firstNodeExists = await comfyPage.page.evaluate(() => {
|
||||
const nodeExists = await comfyPage.page.evaluate(() => {
|
||||
return !!window.app!.graph!.getNodeById('7')
|
||||
})
|
||||
expect(firstNodeExists).toBe(false)
|
||||
expect(nodeExists).toBe(false)
|
||||
|
||||
const secondNodeAfter = await getPseudoPreviewWidgets(comfyPage, '8')
|
||||
expect(secondNodeAfter).toEqual(secondNodeBefore)
|
||||
|
||||
@@ -9,14 +9,18 @@ test.describe('Nested subgraph configure order', { tag: ['@subgraph'] }, () => {
|
||||
test('Loads without "No link found" or "Failed to resolve legacy -1" console warnings', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { warnings } = SubgraphHelper.collectConsoleWarnings(comfyPage.page, [
|
||||
'No link found',
|
||||
'Failed to resolve legacy -1'
|
||||
])
|
||||
const { warnings, dispose } = SubgraphHelper.collectConsoleWarnings(
|
||||
comfyPage.page,
|
||||
['No link found', 'Failed to resolve legacy -1']
|
||||
)
|
||||
|
||||
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
||||
try {
|
||||
await comfyPage.workflow.loadWorkflow(WORKFLOW)
|
||||
|
||||
expect(warnings).toEqual([])
|
||||
expect(warnings).toEqual([])
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
|
||||
test('All three subgraph levels resolve promoted widgets', async ({
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -233,7 +233,7 @@ test.describe(
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back to parent graph
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
// Promoted textarea on SubgraphNode should have the same value
|
||||
const promotedTextarea = comfyPage.page.getByTestId(
|
||||
@@ -267,7 +267,7 @@ test.describe(
|
||||
)
|
||||
await expect(interiorTextarea).toHaveValue(testContent)
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
const promotedTextarea = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
@@ -313,7 +313,7 @@ test.describe(
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back to parent
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
// SubgraphNode should now have the promoted widget
|
||||
const widgetCount = await getPromotedWidgetCount(comfyPage, '2')
|
||||
@@ -348,7 +348,7 @@ test.describe(
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back and verify promotion took effect
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
await fitToViewInstant(comfyPage)
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.nextFrame()
|
||||
@@ -380,7 +380,7 @@ test.describe(
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Navigate back to parent
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
// SubgraphNode should have fewer widgets
|
||||
const finalWidgetCount = await getPromotedWidgetCount(comfyPage, '2')
|
||||
@@ -716,7 +716,7 @@ test.describe(
|
||||
|
||||
await comfyPage.subgraph.removeSlot('input')
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
const finalNames = await getPromotedWidgetNames(comfyPage, '5')
|
||||
const expectedNames = [...initialNames]
|
||||
@@ -746,8 +746,8 @@ test.describe(
|
||||
// Remove the text input slot
|
||||
await comfyPage.subgraph.removeSlot('input', 'text')
|
||||
|
||||
// Navigate back via breadcrumb
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
// Navigate back
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
// Widget count should be reduced
|
||||
const finalWidgetCount = await getPromotedWidgetCount(comfyPage, '11')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
async function createSubgraphAndNavigateInto(comfyPage: ComfyPage) {
|
||||
const subgraphNode =
|
||||
await comfyPage.subgraph.convertDefaultKSamplerToSubgraph()
|
||||
await comfyPage.subgraph.loadDefaultAndConvertKSamplerToSubgraph()
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
return subgraphNode
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ test.describe('Subgraph viewport restoration', { tag: '@subgraph' }, () => {
|
||||
await comfyPage.vueNodes.enterSubgraph('11')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.subgraph.exitViaBreadcrumb()
|
||||
await comfyPage.subgraph.exitSubgraph()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user