Skip to content
Closed
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
42 changes: 28 additions & 14 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { VueNodeHelpers } from './VueNodeHelpers'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { PropertiesPanel } from './components/PropertiesPanel'
import { SettingDialog } from './components/SettingDialog'
import {
NodeLibrarySidebarTab,
Expand All @@ -26,32 +27,20 @@ dotenv.config()

type WorkspaceStore = ReturnType<typeof useWorkspaceStore>

class ComfyPropertiesPanel {
readonly root: Locator
readonly panelTitle: Locator
readonly searchBox: Locator

constructor(readonly page: Page) {
this.root = page.getByTestId('properties-panel')
this.panelTitle = this.root.locator('h3')
this.searchBox = this.root.getByPlaceholder('Search...')
}
}

class ComfyMenu {
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
private _workflowsTab: WorkflowsSidebarTab | null = null
private _topbar: Topbar | null = null

public readonly sideToolbar: Locator
public readonly propertiesPanel: ComfyPropertiesPanel
public readonly propertiesPanel: PropertiesPanel
public readonly themeToggleButton: Locator
public readonly saveButton: Locator

constructor(public readonly page: Page) {
this.sideToolbar = page.locator('.side-tool-bar-container')
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
this.propertiesPanel = new ComfyPropertiesPanel(page)
this.propertiesPanel = new PropertiesPanel(page)
this.saveButton = page
.locator('button[title="Save the current workflow"]')
.nth(0)
Expand Down Expand Up @@ -1583,6 +1572,31 @@ export class ComfyPage {
return window['app'].graph.nodes
})
}

async isInSubgraph(): Promise<boolean> {
return await this.page.evaluate(() => {
const graph = window['app'].canvas.graph
return graph?.constructor?.name === 'Subgraph'
})
}

async createNode(
nodeType: string,
position: Position = { x: 200, y: 200 }
): Promise<NodeReference> {
const nodeId = await this.page.evaluate(
({ nodeType, pos }) => {
const node = window['LiteGraph'].createNode(nodeType)
if (!node) throw new Error(`Failed to create node: ${nodeType}`)
window['app'].graph.add(node)
node.pos = [pos.x, pos.y]
return node.id
},
{ nodeType, pos: position }
)
await this.nextFrame()
return this.getNodeRefById(nodeId)
}
async waitForGraphNodes(count: number) {
await this.page.waitForFunction((count) => {
return window['app']?.canvas.graph?.nodes?.length === count
Expand Down
94 changes: 94 additions & 0 deletions browser_tests/fixtures/components/PropertiesPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { Locator, Page } from '@playwright/test'

export class PropertiesPanel {
readonly root: Locator
readonly panelTitle: Locator
readonly searchBox: Locator

constructor(readonly page: Page) {
this.root = page.getByTestId('properties-panel')
this.panelTitle = this.root.locator('h3')
this.searchBox = this.root.getByPlaceholder('Search...')
}

async ensureOpen() {
const isOpen = await this.root.isVisible()
if (!isOpen) {
await this.page.getByLabel('Toggle properties panel').click()
await this.root.waitFor({ state: 'visible' })
}
}

async close() {
const isOpen = await this.root.isVisible()
if (isOpen) {
await this.page.getByLabel('Toggle properties panel').click()
await this.root.waitFor({ state: 'hidden' })
}
}

async promoteWidget(widgetName: string) {
await this.ensureOpen()

// Check if widget is already visible in Advanced Inputs section
const widgetRow = this.root
.locator('[class*="widget-item"], [class*="input-item"]')
.filter({ hasText: widgetName })
.first()
const isAdvancedExpanded = await widgetRow.isVisible()

if (!isAdvancedExpanded) {
// Click on Advanced Inputs to expand it
const advancedInputsButton = this.root
.getByRole('button')
.filter({ hasText: /advanced inputs/i })
await advancedInputsButton.click()
await widgetRow.waitFor({ state: 'visible', timeout: 5000 })
}

// Find and click the more options button
const moreButton = widgetRow.locator('button').filter({
has: this.page.locator('[class*="lucide--more-vertical"]')
})
await moreButton.click()

// Click "Show input" to promote the widget
await this.page.getByText('Show input').click()

// Close and reopen panel to refresh the UI state
await this.close()
await this.ensureOpen()
}

async demoteWidget(widgetName: string) {
await this.ensureOpen()

// Check if INPUTS section content is already visible
const widgetRow = this.root.locator('span').getByText(widgetName).first()
const isInputsExpanded = await widgetRow.isVisible()

if (!isInputsExpanded) {
// Click on INPUTS section to expand it (where promoted widgets appear)
const inputsButton = this.root
.getByRole('button')
.filter({ hasText: /^inputs$/i })
await inputsButton.click()
}

await widgetRow.waitFor({ state: 'visible', timeout: 5000 })

// Find the more options button in the widget-item-header
const moreButton = widgetRow
.locator('xpath=ancestor::*[contains(@class, "widget-item-header")]')
.locator('button')
.filter({
has: this.page.locator('[class*="more-vertical"], [class*="lucide"]')
})
.first()

await moreButton.click()

// Click "Hide input" to demote the widget
await this.page.getByText('Hide input').click()
}
}
5 changes: 5 additions & 0 deletions browser_tests/fixtures/components/Topbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export class Topbar {
await tab.locator('.close-button').click({ force: true })
}

async switchToTab(index: number) {
const tabs = this.page.locator('.workflow-tabs button')
await tabs.nth(index).click()
}

getSaveDialog(): Locator {
return this.page.locator('.p-dialog-content input')
}
Expand Down
60 changes: 59 additions & 1 deletion browser_tests/fixtures/utils/litegraphUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,26 @@ class NodeWidgetReference {
[this.node.id, this.index] as const
)
}

async setValue(value: unknown, useCanvasGraph = false) {
await this.node.comfyPage.page.evaluate(
([id, index, val, useCanvas]) => {
const graph = useCanvas
? window['app'].canvas.graph
: window['app'].graph
const node = graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const widget = node.widgets[index]
if (!widget) throw new Error(`Widget ${index} not found.`)
widget.value = val
if (widget.callback) {
widget.callback(val, window['app'].canvas, node, null, null)
}
},
[this.node.id, this.index, value, useCanvasGraph] as const
)
await this.node.comfyPage.nextFrame()
}
}
export class NodeReference {
constructor(
Expand Down Expand Up @@ -339,8 +359,43 @@ export class NodeReference {
async getWidget(index: number) {
return new NodeWidgetReference(index, this)
}

async getWidgetByName(
name: string,
useCanvasGraph = false
): Promise<NodeWidgetReference | null> {
const index = await this.comfyPage.page.evaluate(
([id, widgetName, useCanvas]) => {
const graph = useCanvas
? window['app'].canvas.graph
: window['app'].graph
const node = graph.getNodeById(id)
if (!node?.widgets) return -1
return node.widgets.findIndex(
(w: { name: string }) => w.name === widgetName
)
},
[this.id, name, useCanvasGraph] as const
)
if (index === -1) return null
return new NodeWidgetReference(index, this)
}

async getWidgets(): Promise<
Array<{ name: string; visible: boolean; value: unknown }>
> {
return await this.comfyPage.page.evaluate((id) => {
const node = window['app'].graph.getNodeById(id)
if (!node?.widgets) return []

return node.widgets.map((w) => {
const isHidden = w.hidden === true || w.options?.hidden === true
return { name: w.name, visible: !isHidden, value: w.value }
})
}, this.id)
}
async click(
position: 'title' | 'collapse',
position: 'title' | 'collapse' | 'subgraph',
options?: Parameters<Page['click']>[1] & { moveMouseToEmptyArea?: boolean }
) {
const nodePos = await this.getPosition()
Expand All @@ -353,6 +408,9 @@ export class NodeReference {
case 'collapse':
clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 }
break
case 'subgraph':
clickPos = { x: nodePos.x + nodeSize.width - 15, y: nodePos.y - 15 }
break
default:
throw new Error(`Invalid click position ${position}`)
}
Expand Down
Loading