Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
97 changes: 97 additions & 0 deletions browser_tests/fixtures/components/PropertiesPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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()

// Click on Advanced Inputs to expand it
const advancedInputsButton = this.root
.getByRole('button')
.filter({ hasText: /advanced inputs/i })
await advancedInputsButton.click()

// Find the widget row and click the more options button
const widgetRow = this.root
.locator('[class*="widget-item"], [class*="input-item"]')
.filter({ hasText: widgetName })
.first()

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.page.getByLabel('Toggle properties panel').click()
await this.page.getByLabel('Toggle properties panel').click()
}
Comment thread
pythongosssss marked this conversation as resolved.

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

// Check if INPUTS section content is already visible
const inputsContent = this.root.locator('div').filter({
hasText: new RegExp(`^${widgetName}$`)
})
const isInputsExpanded = await inputsContent.first().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()
}

// Find the widget row and click the more options button
const widgetRow = this.root
.locator('div')
.filter({ hasText: new RegExp(`^${widgetName}$`) })
.first()
Comment thread
pythongosssss marked this conversation as resolved.
Outdated

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

// Find the more options button (the vertical dots icon button)
const moreButton = widgetRow
.locator('..')
.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