mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
Split ComfyPage fixture (#1305)
* Split down page components * Move litegraph utils * nit
This commit is contained in:
@@ -8,280 +8,15 @@ import * as fs from 'fs'
|
|||||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||||
import type { NodeId } from '../../src/types/comfyWorkflow'
|
import type { NodeId } from '../../src/types/comfyWorkflow'
|
||||||
import type { KeyCombo } from '../../src/types/keyBindingTypes'
|
import type { KeyCombo } from '../../src/types/keyBindingTypes'
|
||||||
import { ManageGroupNode } from '../helpers/manageGroupNode'
|
|
||||||
import { ComfyTemplates } from '../helpers/templates'
|
import { ComfyTemplates } from '../helpers/templates'
|
||||||
|
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||||
interface Position {
|
import {
|
||||||
x: number
|
NodeLibrarySidebarTab,
|
||||||
y: number
|
WorkflowsSidebarTab
|
||||||
}
|
} from './components/SidebarTab'
|
||||||
|
import { Topbar } from './components/Topbar'
|
||||||
interface Size {
|
import type { NodeReference } from './utils/litegraphUtils'
|
||||||
width: number
|
import type { Position, Size } from './types'
|
||||||
height: number
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComfyNodeSearchFilterSelectionPanel {
|
|
||||||
constructor(public readonly page: Page) {}
|
|
||||||
|
|
||||||
async selectFilterType(filterType: string) {
|
|
||||||
await this.page
|
|
||||||
.locator(
|
|
||||||
`.filter-type-select .p-togglebutton-label:has-text("${filterType}")`
|
|
||||||
)
|
|
||||||
.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectFilterValue(filterValue: string) {
|
|
||||||
await this.page.locator('.filter-value-select .p-select-dropdown').click()
|
|
||||||
await this.page
|
|
||||||
.locator(
|
|
||||||
`.p-select-overlay .p-select-list .p-select-option-label:text-is("${filterValue}")`
|
|
||||||
)
|
|
||||||
.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async addFilter(filterValue: string, filterType: string) {
|
|
||||||
await this.selectFilterType(filterType)
|
|
||||||
await this.selectFilterValue(filterValue)
|
|
||||||
await this.page.locator('.p-button-label:has-text("Add")').click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComfyNodeSearchBox {
|
|
||||||
public readonly input: Locator
|
|
||||||
public readonly dropdown: Locator
|
|
||||||
public readonly filterSelectionPanel: ComfyNodeSearchFilterSelectionPanel
|
|
||||||
|
|
||||||
constructor(public readonly page: Page) {
|
|
||||||
this.input = page.locator(
|
|
||||||
'.comfy-vue-node-search-container input[type="text"]'
|
|
||||||
)
|
|
||||||
this.dropdown = page.locator(
|
|
||||||
'.comfy-vue-node-search-container .p-autocomplete-list'
|
|
||||||
)
|
|
||||||
this.filterSelectionPanel = new ComfyNodeSearchFilterSelectionPanel(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
get filterButton() {
|
|
||||||
return this.page.locator('.comfy-vue-node-search-container ._filter-button')
|
|
||||||
}
|
|
||||||
|
|
||||||
async fillAndSelectFirstNode(
|
|
||||||
nodeName: string,
|
|
||||||
options?: { suggestionIndex: number }
|
|
||||||
) {
|
|
||||||
await this.input.waitFor({ state: 'visible' })
|
|
||||||
await this.input.fill(nodeName)
|
|
||||||
await this.dropdown.waitFor({ state: 'visible' })
|
|
||||||
// Wait for some time for the auto complete list to update.
|
|
||||||
// The auto complete list is debounced and may take some time to update.
|
|
||||||
await this.page.waitForTimeout(500)
|
|
||||||
await this.dropdown
|
|
||||||
.locator('li')
|
|
||||||
.nth(options?.suggestionIndex || 0)
|
|
||||||
.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async addFilter(filterValue: string, filterType: string) {
|
|
||||||
await this.filterButton.click()
|
|
||||||
await this.filterSelectionPanel.addFilter(filterValue, filterType)
|
|
||||||
}
|
|
||||||
|
|
||||||
get filterChips() {
|
|
||||||
return this.page.locator(
|
|
||||||
'.comfy-vue-node-search-container .p-autocomplete-chip-item'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeFilter(index: number) {
|
|
||||||
await this.filterChips.nth(index).locator('.p-chip-remove-icon').click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SidebarTab {
|
|
||||||
constructor(
|
|
||||||
public readonly page: Page,
|
|
||||||
public readonly tabId: string
|
|
||||||
) {}
|
|
||||||
|
|
||||||
get tabButton() {
|
|
||||||
return this.page.locator(`.${this.tabId}-tab-button`)
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedTabButton() {
|
|
||||||
return this.page.locator(
|
|
||||||
`.${this.tabId}-tab-button.side-bar-button-selected`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async open() {
|
|
||||||
if (await this.selectedTabButton.isVisible()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await this.tabButton.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NodeLibrarySidebarTab extends SidebarTab {
|
|
||||||
constructor(public readonly page: Page) {
|
|
||||||
super(page, 'node-library')
|
|
||||||
}
|
|
||||||
|
|
||||||
get nodeLibrarySearchBoxInput() {
|
|
||||||
return this.page.locator('.node-lib-search-box input[type="text"]')
|
|
||||||
}
|
|
||||||
|
|
||||||
get nodeLibraryTree() {
|
|
||||||
return this.page.locator('.node-lib-tree-explorer')
|
|
||||||
}
|
|
||||||
|
|
||||||
get nodePreview() {
|
|
||||||
return this.page.locator('.node-lib-node-preview')
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabContainer() {
|
|
||||||
return this.page.locator('.sidebar-content-container')
|
|
||||||
}
|
|
||||||
|
|
||||||
get newFolderButton() {
|
|
||||||
return this.tabContainer.locator('.new-folder-button')
|
|
||||||
}
|
|
||||||
|
|
||||||
async open() {
|
|
||||||
await super.open()
|
|
||||||
await this.nodeLibraryTree.waitFor({ state: 'visible' })
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
if (!this.tabButton.isVisible()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.tabButton.click()
|
|
||||||
await this.nodeLibraryTree.waitFor({ state: 'hidden' })
|
|
||||||
}
|
|
||||||
|
|
||||||
folderSelector(folderName: string) {
|
|
||||||
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))`
|
|
||||||
}
|
|
||||||
|
|
||||||
getFolder(folderName: string) {
|
|
||||||
return this.page.locator(this.folderSelector(folderName))
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeSelector(nodeName: string) {
|
|
||||||
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))`
|
|
||||||
}
|
|
||||||
|
|
||||||
getNode(nodeName: string) {
|
|
||||||
return this.page.locator(this.nodeSelector(nodeName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkflowsSidebarTab extends SidebarTab {
|
|
||||||
constructor(public readonly page: Page) {
|
|
||||||
super(page, 'workflows')
|
|
||||||
}
|
|
||||||
|
|
||||||
get browseGalleryButton() {
|
|
||||||
return this.page.locator('.browse-templates-button')
|
|
||||||
}
|
|
||||||
|
|
||||||
get newBlankWorkflowButton() {
|
|
||||||
return this.page.locator('.new-blank-workflow-button')
|
|
||||||
}
|
|
||||||
|
|
||||||
get openWorkflowButton() {
|
|
||||||
return this.page.locator('.open-workflow-button')
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOpenedWorkflowNames() {
|
|
||||||
return await this.page
|
|
||||||
.locator('.comfyui-workflows-open .node-label')
|
|
||||||
.allInnerTexts()
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTopLevelSavedWorkflowNames() {
|
|
||||||
return await this.page
|
|
||||||
.locator('.comfyui-workflows-browse .node-label')
|
|
||||||
.allInnerTexts()
|
|
||||||
}
|
|
||||||
|
|
||||||
async switchToWorkflow(workflowName: string) {
|
|
||||||
const workflowLocator = this.page.locator(
|
|
||||||
'.comfyui-workflows-open .node-label',
|
|
||||||
{ hasText: workflowName }
|
|
||||||
)
|
|
||||||
await workflowLocator.click()
|
|
||||||
await this.page.waitForTimeout(300)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Topbar {
|
|
||||||
constructor(public readonly page: Page) {}
|
|
||||||
|
|
||||||
async getTabNames(): Promise<string[]> {
|
|
||||||
return await this.page
|
|
||||||
.locator('.workflow-tabs .workflow-label')
|
|
||||||
.allInnerTexts()
|
|
||||||
}
|
|
||||||
|
|
||||||
async openSubmenuMobile() {
|
|
||||||
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMenuItem(itemLabel: string): Promise<Locator> {
|
|
||||||
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWorkflowTab(tabName: string): Promise<Locator> {
|
|
||||||
return this.page
|
|
||||||
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
|
||||||
.locator('..')
|
|
||||||
}
|
|
||||||
|
|
||||||
async closeWorkflowTab(tabName: string) {
|
|
||||||
const tab = await this.getWorkflowTab(tabName)
|
|
||||||
await tab.locator('.close-button').click({ force: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveWorkflow(workflowName: string) {
|
|
||||||
await this.triggerTopbarCommand(['Workflow', 'Save'])
|
|
||||||
await this.page.locator('.p-dialog-content input').fill(workflowName)
|
|
||||||
await this.page.keyboard.press('Enter')
|
|
||||||
// Wait for the dialog to close.
|
|
||||||
await this.page.waitForTimeout(300)
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerTopbarCommand(path: string[]) {
|
|
||||||
if (path.length < 2) {
|
|
||||||
throw new Error('Path is too short')
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabName = path[0]
|
|
||||||
const topLevelMenu = this.page.locator(
|
|
||||||
`.top-menubar .p-menubar-item-label:text-is("${tabName}")`
|
|
||||||
)
|
|
||||||
await topLevelMenu.waitFor({ state: 'visible' })
|
|
||||||
await topLevelMenu.click()
|
|
||||||
|
|
||||||
for (let i = 1; i < path.length; i++) {
|
|
||||||
const commandName = path[i]
|
|
||||||
const menuItem = this.page
|
|
||||||
.locator(
|
|
||||||
`.top-menubar .p-menubar-submenu .p-menubar-item:has-text("${commandName}")`
|
|
||||||
)
|
|
||||||
.first()
|
|
||||||
await menuItem.waitFor({ state: 'visible' })
|
|
||||||
await menuItem.hover()
|
|
||||||
|
|
||||||
if (i === path.length - 1) {
|
|
||||||
await menuItem.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComfyMenu {
|
class ComfyMenu {
|
||||||
public readonly sideToolbar: Locator
|
public readonly sideToolbar: Locator
|
||||||
@@ -984,258 +719,6 @@ export class ComfyPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NodeSlotReference {
|
|
||||||
constructor(
|
|
||||||
readonly type: 'input' | 'output',
|
|
||||||
readonly index: number,
|
|
||||||
readonly node: NodeReference
|
|
||||||
) {}
|
|
||||||
async getPosition() {
|
|
||||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
|
||||||
([type, id, index]) => {
|
|
||||||
const node = window['app'].graph.getNodeById(id)
|
|
||||||
if (!node) throw new Error(`Node ${id} not found.`)
|
|
||||||
return window['app'].canvas.ds.convertOffsetToCanvas(
|
|
||||||
node.getConnectionPos(type === 'input', index)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[this.type, this.node.id, this.index] as const
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
x: pos[0],
|
|
||||||
y: pos[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async getLinkCount() {
|
|
||||||
return await this.node.comfyPage.page.evaluate(
|
|
||||||
([type, id, index]) => {
|
|
||||||
const node = window['app'].graph.getNodeById(id)
|
|
||||||
if (!node) throw new Error(`Node ${id} not found.`)
|
|
||||||
if (type === 'input') {
|
|
||||||
return node.inputs[index].link == null ? 0 : 1
|
|
||||||
}
|
|
||||||
return node.outputs[index].links?.length ?? 0
|
|
||||||
},
|
|
||||||
[this.type, this.node.id, this.index] as const
|
|
||||||
)
|
|
||||||
}
|
|
||||||
async removeLinks() {
|
|
||||||
await this.node.comfyPage.page.evaluate(
|
|
||||||
([type, id, index]) => {
|
|
||||||
const node = window['app'].graph.getNodeById(id)
|
|
||||||
if (!node) throw new Error(`Node ${id} not found.`)
|
|
||||||
if (type === 'input') {
|
|
||||||
node.disconnectInput(index)
|
|
||||||
} else {
|
|
||||||
node.disconnectOutput(index)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[this.type, this.node.id, this.index] as const
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NodeWidgetReference {
|
|
||||||
constructor(
|
|
||||||
readonly index: number,
|
|
||||||
readonly node: NodeReference
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getPosition(): Promise<Position> {
|
|
||||||
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
|
||||||
([id, index]) => {
|
|
||||||
const node = window['app'].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.`)
|
|
||||||
|
|
||||||
const [x, y, w, h] = node.getBounding()
|
|
||||||
return window['app'].canvas.ds.convertOffsetToCanvas([
|
|
||||||
x + w / 2,
|
|
||||||
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
|
|
||||||
])
|
|
||||||
},
|
|
||||||
[this.node.id, this.index] as const
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
x: pos[0],
|
|
||||||
y: pos[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NodeReference {
|
|
||||||
constructor(
|
|
||||||
readonly id: NodeId,
|
|
||||||
readonly comfyPage: ComfyPage
|
|
||||||
) {}
|
|
||||||
async exists(): Promise<boolean> {
|
|
||||||
return await this.comfyPage.page.evaluate((id) => {
|
|
||||||
const node = window['app'].graph.getNodeById(id)
|
|
||||||
return !!node
|
|
||||||
}, this.id)
|
|
||||||
}
|
|
||||||
getType(): Promise<string> {
|
|
||||||
return this.getProperty('type')
|
|
||||||
}
|
|
||||||
async getPosition(): Promise<Position> {
|
|
||||||
const pos = await this.comfyPage.convertOffsetToCanvas(
|
|
||||||
await this.getProperty<[number, number]>('pos')
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
x: pos[0],
|
|
||||||
y: pos[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async getBounding(): Promise<Position & Size> {
|
|
||||||
const [x, y, width, height]: [number, number, number, number] =
|
|
||||||
await this.comfyPage.page.evaluate((id) => {
|
|
||||||
const node = window['app'].graph.getNodeById(id)
|
|
||||||
if (!node) throw new Error('Node not found')
|
|
||||||
return node.getBounding()
|
|
||||||
}, this.id)
|
|
||||||
return {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async getSize(): Promise<Size> {
|
|
||||||
const size = await this.getProperty<[number, number]>('size')
|
|
||||||
return {
|
|
||||||
width: size[0],
|
|
||||||
height: size[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async getFlags(): Promise<{ collapsed?: boolean; pinned?: boolean }> {
|
|
||||||
return await this.getProperty('flags')
|
|
||||||
}
|
|
||||||
async isPinned() {
|
|
||||||
return !!(await this.getFlags()).pinned
|
|
||||||
}
|
|
||||||
async isCollapsed() {
|
|
||||||
return !!(await this.getFlags()).collapsed
|
|
||||||
}
|
|
||||||
async isBypassed() {
|
|
||||||
return (await this.getProperty<number | null | undefined>('mode')) === 4
|
|
||||||
}
|
|
||||||
async getProperty<T>(prop: string): Promise<T> {
|
|
||||||
return await this.comfyPage.page.evaluate(
|
|
||||||
([id, prop]) => {
|
|
||||||
const node = window['app'].graph.getNodeById(id)
|
|
||||||
if (!node) throw new Error('Node not found')
|
|
||||||
return node[prop]
|
|
||||||
},
|
|
||||||
[this.id, prop] as const
|
|
||||||
)
|
|
||||||
}
|
|
||||||
async getOutput(index: number) {
|
|
||||||
return new NodeSlotReference('output', index, this)
|
|
||||||
}
|
|
||||||
async getInput(index: number) {
|
|
||||||
return new NodeSlotReference('input', index, this)
|
|
||||||
}
|
|
||||||
async getWidget(index: number) {
|
|
||||||
return new NodeWidgetReference(index, this)
|
|
||||||
}
|
|
||||||
async click(
|
|
||||||
position: 'title' | 'collapse',
|
|
||||||
options?: Parameters<Page['click']>[1] & { moveMouseToEmptyArea?: boolean }
|
|
||||||
) {
|
|
||||||
const nodePos = await this.getPosition()
|
|
||||||
const nodeSize = await this.getSize()
|
|
||||||
let clickPos: Position
|
|
||||||
switch (position) {
|
|
||||||
case 'title':
|
|
||||||
clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 }
|
|
||||||
break
|
|
||||||
case 'collapse':
|
|
||||||
clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 }
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error(`Invalid click position ${position}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveMouseToEmptyArea = options?.moveMouseToEmptyArea
|
|
||||||
if (options) {
|
|
||||||
delete options.moveMouseToEmptyArea
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.comfyPage.canvas.click({
|
|
||||||
...options,
|
|
||||||
position: clickPos
|
|
||||||
})
|
|
||||||
await this.comfyPage.nextFrame()
|
|
||||||
if (moveMouseToEmptyArea) {
|
|
||||||
await this.comfyPage.moveMouseToEmptyArea()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async copy() {
|
|
||||||
await this.click('title')
|
|
||||||
await this.comfyPage.ctrlC()
|
|
||||||
await this.comfyPage.nextFrame()
|
|
||||||
}
|
|
||||||
async connectWidget(
|
|
||||||
originSlotIndex: number,
|
|
||||||
targetNode: NodeReference,
|
|
||||||
targetWidgetIndex: number
|
|
||||||
) {
|
|
||||||
const originSlot = await this.getOutput(originSlotIndex)
|
|
||||||
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
|
|
||||||
await this.comfyPage.dragAndDrop(
|
|
||||||
await originSlot.getPosition(),
|
|
||||||
await targetWidget.getPosition()
|
|
||||||
)
|
|
||||||
return originSlot
|
|
||||||
}
|
|
||||||
async connectOutput(
|
|
||||||
originSlotIndex: number,
|
|
||||||
targetNode: NodeReference,
|
|
||||||
targetSlotIndex: number
|
|
||||||
) {
|
|
||||||
const originSlot = await this.getOutput(originSlotIndex)
|
|
||||||
const targetSlot = await targetNode.getInput(targetSlotIndex)
|
|
||||||
await this.comfyPage.dragAndDrop(
|
|
||||||
await originSlot.getPosition(),
|
|
||||||
await targetSlot.getPosition()
|
|
||||||
)
|
|
||||||
return originSlot
|
|
||||||
}
|
|
||||||
async getContextMenuOptionNames() {
|
|
||||||
await this.click('title', { button: 'right' })
|
|
||||||
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
|
||||||
return await ctx.locator('.litemenu-entry').allInnerTexts()
|
|
||||||
}
|
|
||||||
async clickContextMenuOption(optionText: string) {
|
|
||||||
await this.click('title', { button: 'right' })
|
|
||||||
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
|
||||||
await ctx.getByText(optionText).click()
|
|
||||||
}
|
|
||||||
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
|
|
||||||
this.comfyPage.page.once('dialog', async (dialog) => {
|
|
||||||
await dialog.accept(groupNodeName)
|
|
||||||
})
|
|
||||||
await this.clickContextMenuOption('Convert to Group Node')
|
|
||||||
await this.comfyPage.nextFrame()
|
|
||||||
const nodes = await this.comfyPage.getNodeRefsByType(
|
|
||||||
`workflow>${groupNodeName}`
|
|
||||||
)
|
|
||||||
if (nodes.length !== 1) {
|
|
||||||
throw new Error(`Did not find single group node (found=${nodes.length})`)
|
|
||||||
}
|
|
||||||
return nodes[0]
|
|
||||||
}
|
|
||||||
async manageGroupNode() {
|
|
||||||
await this.clickContextMenuOption('Manage Group Node')
|
|
||||||
await this.comfyPage.nextFrame()
|
|
||||||
return new ManageGroupNode(
|
|
||||||
this.comfyPage.page,
|
|
||||||
this.comfyPage.page.locator('.comfy-group-manage')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
|
||||||
comfyPage: async ({ page, request }, use) => {
|
comfyPage: async ({ page, request }, use) => {
|
||||||
const comfyPage = new ComfyPage(page, request)
|
const comfyPage = new ComfyPage(page, request)
|
||||||
|
|||||||
79
browser_tests/fixtures/components/ComfyNodeSearchBox.ts
Normal file
79
browser_tests/fixtures/components/ComfyNodeSearchBox.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
export class ComfyNodeSearchFilterSelectionPanel {
|
||||||
|
constructor(public readonly page: Page) {}
|
||||||
|
|
||||||
|
async selectFilterType(filterType: string) {
|
||||||
|
await this.page
|
||||||
|
.locator(
|
||||||
|
`.filter-type-select .p-togglebutton-label:has-text("${filterType}")`
|
||||||
|
)
|
||||||
|
.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectFilterValue(filterValue: string) {
|
||||||
|
await this.page.locator('.filter-value-select .p-select-dropdown').click()
|
||||||
|
await this.page
|
||||||
|
.locator(
|
||||||
|
`.p-select-overlay .p-select-list .p-select-option-label:text-is("${filterValue}")`
|
||||||
|
)
|
||||||
|
.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async addFilter(filterValue: string, filterType: string) {
|
||||||
|
await this.selectFilterType(filterType)
|
||||||
|
await this.selectFilterValue(filterValue)
|
||||||
|
await this.page.locator('.p-button-label:has-text("Add")').click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ComfyNodeSearchBox {
|
||||||
|
public readonly input: Locator
|
||||||
|
public readonly dropdown: Locator
|
||||||
|
public readonly filterSelectionPanel: ComfyNodeSearchFilterSelectionPanel
|
||||||
|
|
||||||
|
constructor(public readonly page: Page) {
|
||||||
|
this.input = page.locator(
|
||||||
|
'.comfy-vue-node-search-container input[type="text"]'
|
||||||
|
)
|
||||||
|
this.dropdown = page.locator(
|
||||||
|
'.comfy-vue-node-search-container .p-autocomplete-list'
|
||||||
|
)
|
||||||
|
this.filterSelectionPanel = new ComfyNodeSearchFilterSelectionPanel(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterButton() {
|
||||||
|
return this.page.locator('.comfy-vue-node-search-container ._filter-button')
|
||||||
|
}
|
||||||
|
|
||||||
|
async fillAndSelectFirstNode(
|
||||||
|
nodeName: string,
|
||||||
|
options?: { suggestionIndex: number }
|
||||||
|
) {
|
||||||
|
await this.input.waitFor({ state: 'visible' })
|
||||||
|
await this.input.fill(nodeName)
|
||||||
|
await this.dropdown.waitFor({ state: 'visible' })
|
||||||
|
// Wait for some time for the auto complete list to update.
|
||||||
|
// The auto complete list is debounced and may take some time to update.
|
||||||
|
await this.page.waitForTimeout(500)
|
||||||
|
await this.dropdown
|
||||||
|
.locator('li')
|
||||||
|
.nth(options?.suggestionIndex || 0)
|
||||||
|
.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async addFilter(filterValue: string, filterType: string) {
|
||||||
|
await this.filterButton.click()
|
||||||
|
await this.filterSelectionPanel.addFilter(filterValue, filterType)
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterChips() {
|
||||||
|
return this.page.locator(
|
||||||
|
'.comfy-vue-node-search-container .p-autocomplete-chip-item'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFilter(index: number) {
|
||||||
|
await this.filterChips.nth(index).locator('.p-chip-remove-icon').click()
|
||||||
|
}
|
||||||
|
}
|
||||||
120
browser_tests/fixtures/components/SidebarTab.ts
Normal file
120
browser_tests/fixtures/components/SidebarTab.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { Page } from '@playwright/test'
|
||||||
|
|
||||||
|
class SidebarTab {
|
||||||
|
constructor(
|
||||||
|
public readonly page: Page,
|
||||||
|
public readonly tabId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get tabButton() {
|
||||||
|
return this.page.locator(`.${this.tabId}-tab-button`)
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedTabButton() {
|
||||||
|
return this.page.locator(
|
||||||
|
`.${this.tabId}-tab-button.side-bar-button-selected`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async open() {
|
||||||
|
if (await this.selectedTabButton.isVisible()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await this.tabButton.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodeLibrarySidebarTab extends SidebarTab {
|
||||||
|
constructor(public readonly page: Page) {
|
||||||
|
super(page, 'node-library')
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodeLibrarySearchBoxInput() {
|
||||||
|
return this.page.locator('.node-lib-search-box input[type="text"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodeLibraryTree() {
|
||||||
|
return this.page.locator('.node-lib-tree-explorer')
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodePreview() {
|
||||||
|
return this.page.locator('.node-lib-node-preview')
|
||||||
|
}
|
||||||
|
|
||||||
|
get tabContainer() {
|
||||||
|
return this.page.locator('.sidebar-content-container')
|
||||||
|
}
|
||||||
|
|
||||||
|
get newFolderButton() {
|
||||||
|
return this.tabContainer.locator('.new-folder-button')
|
||||||
|
}
|
||||||
|
|
||||||
|
async open() {
|
||||||
|
await super.open()
|
||||||
|
await this.nodeLibraryTree.waitFor({ state: 'visible' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
if (!this.tabButton.isVisible()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.tabButton.click()
|
||||||
|
await this.nodeLibraryTree.waitFor({ state: 'hidden' })
|
||||||
|
}
|
||||||
|
|
||||||
|
folderSelector(folderName: string) {
|
||||||
|
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))`
|
||||||
|
}
|
||||||
|
|
||||||
|
getFolder(folderName: string) {
|
||||||
|
return this.page.locator(this.folderSelector(folderName))
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeSelector(nodeName: string) {
|
||||||
|
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))`
|
||||||
|
}
|
||||||
|
|
||||||
|
getNode(nodeName: string) {
|
||||||
|
return this.page.locator(this.nodeSelector(nodeName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkflowsSidebarTab extends SidebarTab {
|
||||||
|
constructor(public readonly page: Page) {
|
||||||
|
super(page, 'workflows')
|
||||||
|
}
|
||||||
|
|
||||||
|
get browseGalleryButton() {
|
||||||
|
return this.page.locator('.browse-templates-button')
|
||||||
|
}
|
||||||
|
|
||||||
|
get newBlankWorkflowButton() {
|
||||||
|
return this.page.locator('.new-blank-workflow-button')
|
||||||
|
}
|
||||||
|
|
||||||
|
get openWorkflowButton() {
|
||||||
|
return this.page.locator('.open-workflow-button')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOpenedWorkflowNames() {
|
||||||
|
return await this.page
|
||||||
|
.locator('.comfyui-workflows-open .node-label')
|
||||||
|
.allInnerTexts()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTopLevelSavedWorkflowNames() {
|
||||||
|
return await this.page
|
||||||
|
.locator('.comfyui-workflows-browse .node-label')
|
||||||
|
.allInnerTexts()
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchToWorkflow(workflowName: string) {
|
||||||
|
const workflowLocator = this.page.locator(
|
||||||
|
'.comfyui-workflows-open .node-label',
|
||||||
|
{ hasText: workflowName }
|
||||||
|
)
|
||||||
|
await workflowLocator.click()
|
||||||
|
await this.page.waitForTimeout(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
browser_tests/fixtures/components/Topbar.ts
Normal file
66
browser_tests/fixtures/components/Topbar.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
export class Topbar {
|
||||||
|
constructor(public readonly page: Page) {}
|
||||||
|
|
||||||
|
async getTabNames(): Promise<string[]> {
|
||||||
|
return await this.page
|
||||||
|
.locator('.workflow-tabs .workflow-label')
|
||||||
|
.allInnerTexts()
|
||||||
|
}
|
||||||
|
|
||||||
|
async openSubmenuMobile() {
|
||||||
|
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMenuItem(itemLabel: string): Promise<Locator> {
|
||||||
|
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWorkflowTab(tabName: string): Promise<Locator> {
|
||||||
|
return this.page
|
||||||
|
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
||||||
|
.locator('..')
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeWorkflowTab(tabName: string) {
|
||||||
|
const tab = await this.getWorkflowTab(tabName)
|
||||||
|
await tab.locator('.close-button').click({ force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveWorkflow(workflowName: string) {
|
||||||
|
await this.triggerTopbarCommand(['Workflow', 'Save'])
|
||||||
|
await this.page.locator('.p-dialog-content input').fill(workflowName)
|
||||||
|
await this.page.keyboard.press('Enter')
|
||||||
|
// Wait for the dialog to close.
|
||||||
|
await this.page.waitForTimeout(300)
|
||||||
|
}
|
||||||
|
|
||||||
|
async triggerTopbarCommand(path: string[]) {
|
||||||
|
if (path.length < 2) {
|
||||||
|
throw new Error('Path is too short')
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabName = path[0]
|
||||||
|
const topLevelMenu = this.page.locator(
|
||||||
|
`.top-menubar .p-menubar-item-label:text-is("${tabName}")`
|
||||||
|
)
|
||||||
|
await topLevelMenu.waitFor({ state: 'visible' })
|
||||||
|
await topLevelMenu.click()
|
||||||
|
|
||||||
|
for (let i = 1; i < path.length; i++) {
|
||||||
|
const commandName = path[i]
|
||||||
|
const menuItem = this.page
|
||||||
|
.locator(
|
||||||
|
`.top-menubar .p-menubar-submenu .p-menubar-item:has-text("${commandName}")`
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
await menuItem.waitFor({ state: 'visible' })
|
||||||
|
await menuItem.hover()
|
||||||
|
|
||||||
|
if (i === path.length - 1) {
|
||||||
|
await menuItem.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
browser_tests/fixtures/types.ts
Normal file
9
browser_tests/fixtures/types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface Position {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
257
browser_tests/fixtures/utils/litegraphUtils.ts
Normal file
257
browser_tests/fixtures/utils/litegraphUtils.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import { ManageGroupNode } from '../../helpers/manageGroupNode'
|
||||||
|
import type { NodeId } from '../../../src/types/comfyWorkflow'
|
||||||
|
import type { Page } from '@playwright/test'
|
||||||
|
import type { ComfyPage } from '../ComfyPage'
|
||||||
|
import type { Position, Size } from '../types'
|
||||||
|
|
||||||
|
export class NodeSlotReference {
|
||||||
|
constructor(
|
||||||
|
readonly type: 'input' | 'output',
|
||||||
|
readonly index: number,
|
||||||
|
readonly node: NodeReference
|
||||||
|
) {}
|
||||||
|
async getPosition() {
|
||||||
|
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||||
|
([type, id, index]) => {
|
||||||
|
const node = window['app'].graph.getNodeById(id)
|
||||||
|
if (!node) throw new Error(`Node ${id} not found.`)
|
||||||
|
return window['app'].canvas.ds.convertOffsetToCanvas(
|
||||||
|
node.getConnectionPos(type === 'input', index)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[this.type, this.node.id, this.index] as const
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
x: pos[0],
|
||||||
|
y: pos[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getLinkCount() {
|
||||||
|
return await this.node.comfyPage.page.evaluate(
|
||||||
|
([type, id, index]) => {
|
||||||
|
const node = window['app'].graph.getNodeById(id)
|
||||||
|
if (!node) throw new Error(`Node ${id} not found.`)
|
||||||
|
if (type === 'input') {
|
||||||
|
return node.inputs[index].link == null ? 0 : 1
|
||||||
|
}
|
||||||
|
return node.outputs[index].links?.length ?? 0
|
||||||
|
},
|
||||||
|
[this.type, this.node.id, this.index] as const
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async removeLinks() {
|
||||||
|
await this.node.comfyPage.page.evaluate(
|
||||||
|
([type, id, index]) => {
|
||||||
|
const node = window['app'].graph.getNodeById(id)
|
||||||
|
if (!node) throw new Error(`Node ${id} not found.`)
|
||||||
|
if (type === 'input') {
|
||||||
|
node.disconnectInput(index)
|
||||||
|
} else {
|
||||||
|
node.disconnectOutput(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[this.type, this.node.id, this.index] as const
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodeWidgetReference {
|
||||||
|
constructor(
|
||||||
|
readonly index: number,
|
||||||
|
readonly node: NodeReference
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getPosition(): Promise<Position> {
|
||||||
|
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
|
||||||
|
([id, index]) => {
|
||||||
|
const node = window['app'].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.`)
|
||||||
|
|
||||||
|
const [x, y, w, h] = node.getBounding()
|
||||||
|
return window['app'].canvas.ds.convertOffsetToCanvas([
|
||||||
|
x + w / 2,
|
||||||
|
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
|
||||||
|
])
|
||||||
|
},
|
||||||
|
[this.node.id, this.index] as const
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
x: pos[0],
|
||||||
|
y: pos[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodeReference {
|
||||||
|
constructor(
|
||||||
|
readonly id: NodeId,
|
||||||
|
readonly comfyPage: ComfyPage
|
||||||
|
) {}
|
||||||
|
async exists(): Promise<boolean> {
|
||||||
|
return await this.comfyPage.page.evaluate((id) => {
|
||||||
|
const node = window['app'].graph.getNodeById(id)
|
||||||
|
return !!node
|
||||||
|
}, this.id)
|
||||||
|
}
|
||||||
|
getType(): Promise<string> {
|
||||||
|
return this.getProperty('type')
|
||||||
|
}
|
||||||
|
async getPosition(): Promise<Position> {
|
||||||
|
const pos = await this.comfyPage.convertOffsetToCanvas(
|
||||||
|
await this.getProperty<[number, number]>('pos')
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
x: pos[0],
|
||||||
|
y: pos[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getBounding(): Promise<Position & Size> {
|
||||||
|
const [x, y, width, height]: [number, number, number, number] =
|
||||||
|
await this.comfyPage.page.evaluate((id) => {
|
||||||
|
const node = window['app'].graph.getNodeById(id)
|
||||||
|
if (!node) throw new Error('Node not found')
|
||||||
|
return node.getBounding()
|
||||||
|
}, this.id)
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getSize(): Promise<Size> {
|
||||||
|
const size = await this.getProperty<[number, number]>('size')
|
||||||
|
return {
|
||||||
|
width: size[0],
|
||||||
|
height: size[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getFlags(): Promise<{ collapsed?: boolean; pinned?: boolean }> {
|
||||||
|
return await this.getProperty('flags')
|
||||||
|
}
|
||||||
|
async isPinned() {
|
||||||
|
return !!(await this.getFlags()).pinned
|
||||||
|
}
|
||||||
|
async isCollapsed() {
|
||||||
|
return !!(await this.getFlags()).collapsed
|
||||||
|
}
|
||||||
|
async isBypassed() {
|
||||||
|
return (await this.getProperty<number | null | undefined>('mode')) === 4
|
||||||
|
}
|
||||||
|
async getProperty<T>(prop: string): Promise<T> {
|
||||||
|
return await this.comfyPage.page.evaluate(
|
||||||
|
([id, prop]) => {
|
||||||
|
const node = window['app'].graph.getNodeById(id)
|
||||||
|
if (!node) throw new Error('Node not found')
|
||||||
|
return node[prop]
|
||||||
|
},
|
||||||
|
[this.id, prop] as const
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async getOutput(index: number) {
|
||||||
|
return new NodeSlotReference('output', index, this)
|
||||||
|
}
|
||||||
|
async getInput(index: number) {
|
||||||
|
return new NodeSlotReference('input', index, this)
|
||||||
|
}
|
||||||
|
async getWidget(index: number) {
|
||||||
|
return new NodeWidgetReference(index, this)
|
||||||
|
}
|
||||||
|
async click(
|
||||||
|
position: 'title' | 'collapse',
|
||||||
|
options?: Parameters<Page['click']>[1] & { moveMouseToEmptyArea?: boolean }
|
||||||
|
) {
|
||||||
|
const nodePos = await this.getPosition()
|
||||||
|
const nodeSize = await this.getSize()
|
||||||
|
let clickPos: Position
|
||||||
|
switch (position) {
|
||||||
|
case 'title':
|
||||||
|
clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 }
|
||||||
|
break
|
||||||
|
case 'collapse':
|
||||||
|
clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 }
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid click position ${position}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveMouseToEmptyArea = options?.moveMouseToEmptyArea
|
||||||
|
if (options) {
|
||||||
|
delete options.moveMouseToEmptyArea
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.comfyPage.canvas.click({
|
||||||
|
...options,
|
||||||
|
position: clickPos
|
||||||
|
})
|
||||||
|
await this.comfyPage.nextFrame()
|
||||||
|
if (moveMouseToEmptyArea) {
|
||||||
|
await this.comfyPage.moveMouseToEmptyArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async copy() {
|
||||||
|
await this.click('title')
|
||||||
|
await this.comfyPage.ctrlC()
|
||||||
|
await this.comfyPage.nextFrame()
|
||||||
|
}
|
||||||
|
async connectWidget(
|
||||||
|
originSlotIndex: number,
|
||||||
|
targetNode: NodeReference,
|
||||||
|
targetWidgetIndex: number
|
||||||
|
) {
|
||||||
|
const originSlot = await this.getOutput(originSlotIndex)
|
||||||
|
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
|
||||||
|
await this.comfyPage.dragAndDrop(
|
||||||
|
await originSlot.getPosition(),
|
||||||
|
await targetWidget.getPosition()
|
||||||
|
)
|
||||||
|
return originSlot
|
||||||
|
}
|
||||||
|
async connectOutput(
|
||||||
|
originSlotIndex: number,
|
||||||
|
targetNode: NodeReference,
|
||||||
|
targetSlotIndex: number
|
||||||
|
) {
|
||||||
|
const originSlot = await this.getOutput(originSlotIndex)
|
||||||
|
const targetSlot = await targetNode.getInput(targetSlotIndex)
|
||||||
|
await this.comfyPage.dragAndDrop(
|
||||||
|
await originSlot.getPosition(),
|
||||||
|
await targetSlot.getPosition()
|
||||||
|
)
|
||||||
|
return originSlot
|
||||||
|
}
|
||||||
|
async getContextMenuOptionNames() {
|
||||||
|
await this.click('title', { button: 'right' })
|
||||||
|
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
||||||
|
return await ctx.locator('.litemenu-entry').allInnerTexts()
|
||||||
|
}
|
||||||
|
async clickContextMenuOption(optionText: string) {
|
||||||
|
await this.click('title', { button: 'right' })
|
||||||
|
const ctx = this.comfyPage.page.locator('.litecontextmenu')
|
||||||
|
await ctx.getByText(optionText).click()
|
||||||
|
}
|
||||||
|
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
|
||||||
|
this.comfyPage.page.once('dialog', async (dialog) => {
|
||||||
|
await dialog.accept(groupNodeName)
|
||||||
|
})
|
||||||
|
await this.clickContextMenuOption('Convert to Group Node')
|
||||||
|
await this.comfyPage.nextFrame()
|
||||||
|
const nodes = await this.comfyPage.getNodeRefsByType(
|
||||||
|
`workflow>${groupNodeName}`
|
||||||
|
)
|
||||||
|
if (nodes.length !== 1) {
|
||||||
|
throw new Error(`Did not find single group node (found=${nodes.length})`)
|
||||||
|
}
|
||||||
|
return nodes[0]
|
||||||
|
}
|
||||||
|
async manageGroupNode() {
|
||||||
|
await this.clickContextMenuOption('Manage Group Node')
|
||||||
|
await this.comfyPage.nextFrame()
|
||||||
|
return new ManageGroupNode(
|
||||||
|
this.comfyPage.page,
|
||||||
|
this.comfyPage.page.locator('.comfy-group-manage')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
import {
|
import { ComfyPage, comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||||
ComfyPage,
|
import type { NodeReference } from './fixtures/utils/litegraphUtils'
|
||||||
NodeReference,
|
|
||||||
comfyPageFixture as test
|
|
||||||
} from './fixtures/ComfyPage'
|
|
||||||
|
|
||||||
test.describe('Group Node', () => {
|
test.describe('Group Node', () => {
|
||||||
test.afterEach(async ({ comfyPage }) => {
|
test.afterEach(async ({ comfyPage }) => {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
import {
|
import { comfyPageFixture as test } from './fixtures/ComfyPage'
|
||||||
type NodeReference,
|
import type { NodeReference } from './fixtures/utils/litegraphUtils'
|
||||||
comfyPageFixture as test
|
|
||||||
} from './fixtures/ComfyPage'
|
|
||||||
|
|
||||||
test.describe('Primitive Node', () => {
|
test.describe('Primitive Node', () => {
|
||||||
test('Can load with correct size', async ({ comfyPage }) => {
|
test('Can load with correct size', async ({ comfyPage }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user