Add (premptive) test for reordering linekd widgets

Requires extensive fixture updates
This commit is contained in:
Austin
2026-05-07 22:07:16 -07:00
parent 8928959a09
commit 569450fd5c
5 changed files with 163 additions and 47 deletions

View File

@@ -66,6 +66,12 @@ export class ComfyMouse implements Omit<Mouse, 'move'> {
await this.drop(options)
}
async getCenter(locator: Locator): Promise<Position> {
const bounds = await locator.boundingBox()
if (!bounds) throw new Error('Failed to get bounds' + locator)
return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 }
}
/** @see {@link Mouse.move} */
async move(to: Position, options = ComfyMouse.defaultOptions) {
await this.mouse.move(to.x, to.y, options)

View File

@@ -249,4 +249,33 @@ export class VueNodeHelpers {
position: { x: box.width / 2, y: box.height * 0.75 }
})
}
getSlot(name: string | Locator, node?: string | Locator) {
const parentLocator = !node
? this.page
: typeof node === 'string'
? this.getNodeByTitle(node)
: node
const slotLocators = parentLocator
.getByTestId('node-widget')
.or(parentLocator.locator('.lg-slot'))
const filteredLocator =
typeof name === 'string'
? slotLocators.filter({ hasText: name })
: slotLocators.filter({ has: name })
return filteredLocator.getByTestId('slot-dot').locator('..')
}
async isSlotConnected(slot: Locator) {
const key = await slot.getByTestId('slot-dot').getAttribute('data-slot-key')
if (!key) return false
return await this.page.evaluate((key) => {
const [nodeId, type, slotId] = key.split('-')
const node = app?.canvas?.graph?.getNodeById(nodeId)
if (!node) return false
return type === 'in'
? node.inputs[Number(slotId)]?.link !== null
: !!node.outputs[Number(slotId)].links?.length
}, key)
}
}

View File

@@ -0,0 +1,71 @@
import type { Locator } from '@playwright/test'
import { comfyExpect as expect } from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
export class SubgraphEditor {
public readonly root
constructor(protected readonly comfyPage: ComfyPage) {
this.root = this.comfyPage.menu.propertiesPanel.root
}
async open(subgraphNode: Locator) {
await this.comfyPage.vueNodes.selectNodeByLocator(subgraphNode)
// TODO: don't use commands for this
await this.comfyPage.command.executeCommand(
'Comfy.Graph.EditSubgraphWidgets'
)
await expect(this.root, 'Open Properties Panel').toBeVisible()
}
resolvePromotionItem(options: {
nodeName?: string
nodeId?: string
widgetName: string
}): Locator {
const labelLocator = this.comfyPage.page
.getByTestId(TestIds.subgraphEditor.widgetLabel)
.filter({ hasText: options.widgetName })
const named = this.root
.getByTestId(TestIds.subgraphEditor.widgetItem)
.filter({ has: labelLocator })
if (!options.nodeName && !options.nodeId) return named
if (options.nodeName) {
const nodeNameLocator = this.comfyPage.page
.getByTestId(TestIds.subgraphEditor.nodeName)
.filter({ hasText: options.nodeName })
return named.filter({ has: nodeNameLocator })
}
const idLocator = this.comfyPage.page.locator(
`[data-nodeid="${options.nodeId}"]`
)
return named.filter({ has: idLocator })
}
async togglePromotionOnItem(item: Locator, toState?: boolean) {
const toggleButton = item.getByTestId(TestIds.subgraphEditor.iconEye)
if (toState !== undefined) {
const expectedIcon = `icon-[lucide--eye${toState ? '-off' : ''}]`
await expect(toggleButton).toContainClass(expectedIcon)
}
await toggleButton.click()
}
async togglePromotion(
subgraphNode: Locator,
options: {
nodeName?: string
nodeId?: string
widgetName: string
toState?: boolean
}
) {
await this.open(subgraphNode)
const item = this.resolvePromotionItem(options)
await this.togglePromotionOnItem(item, options.toState)
}
}

View File

@@ -9,12 +9,17 @@ import type { ComfyWorkflow } from '@/platform/workflow/management/stores/comfyW
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { SubgraphEditor } from '@e2e/fixtures/components/SubgraphEditor'
import { TestIds } from '@e2e/fixtures/selectors'
import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils'
import { SubgraphSlotReference } from '@e2e/fixtures/utils/litegraphUtils'
export class SubgraphHelper {
constructor(private readonly comfyPage: ComfyPage) {}
public readonly editor: SubgraphEditor
constructor(private readonly comfyPage: ComfyPage) {
this.editor = new SubgraphEditor(comfyPage)
}
private get page(): Page {
return this.comfyPage.page
@@ -327,50 +332,6 @@ export class SubgraphHelper {
await this.comfyPage.nextFrame()
}
async toggleContainedWidgetPromotion(
subgraphNode: Locator,
options: (
| { nodeName: string; nodeId?: undefined }
| { nodeName?: undefined; nodeId: string }
) & { widgetName: string; toState?: boolean }
) {
await this.comfyPage.vueNodes.selectNodeByLocator(subgraphNode)
await this.comfyPage.command.executeCommand(
'Comfy.Graph.EditSubgraphWidgets'
)
const { root } = this.comfyPage.menu.propertiesPanel
await expect(root, 'Open Properties Panel').toBeVisible()
const resolvePromotionItem: () => Locator = () => {
const labelLocator = this.comfyPage.page
.getByTestId(TestIds.subgraphEditor.widgetLabel)
.filter({ hasText: options.widgetName })
const named = root
.getByTestId(TestIds.subgraphEditor.widgetItem)
.filter({ has: labelLocator })
if (options.nodeName !== undefined) {
const nodeNameLocator = this.comfyPage.page
.getByTestId(TestIds.subgraphEditor.nodeName)
.filter({ hasText: options.nodeName })
return named.filter({ has: nodeNameLocator })
}
const idLocator = this.comfyPage.page.locator(
`[data-nodeid="${options.nodeId}"]`
)
return named.filter({ has: idLocator })
}
const item = resolvePromotionItem()
const toggleButton = item.getByTestId(TestIds.subgraphEditor.iconEye)
if (options.toState !== undefined) {
const expectedIcon = `icon-[lucide--eye${options.toState ? '-off' : ''}]`
await expect(toggleButton).toContainClass(expectedIcon)
}
await toggleButton.click()
}
async promoteWidget(nodeLocator: Locator, widgetName: string): Promise<void> {
const widget = nodeLocator.getByLabel(widgetName, { exact: true })
await this.comfyPage.contextMenu

View File

@@ -637,17 +637,66 @@ test('@vue-nodes Promote/Demote by side panel', async ({ comfyPage }) => {
const subgraphNode = comfyPage.vueNodes.getNodeLocator('2')
const steps = comfyPage.vueNodes.getWidgetByName('New Subgraph', 'steps')
await comfyPage.subgraph.toggleContainedWidgetPromotion(subgraphNode, {
await comfyPage.subgraph.editor.togglePromotion(subgraphNode, {
nodeName: 'KSampler',
widgetName: 'steps',
toState: true
})
await expect(steps, 'Promote widget').toBeVisible()
await comfyPage.subgraph.toggleContainedWidgetPromotion(subgraphNode, {
await comfyPage.subgraph.editor.togglePromotion(subgraphNode, {
nodeName: 'KSampler',
widgetName: 'steps',
toState: false
})
await expect(steps, 'Un-promote widget').toBeHidden()
})
test('@vue-nodes Can intermix linked and proxy', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = comfyPage.vueNodes.getNodeLocator('2')
await test.step('Enter subgraph and link widget to input', async () => {
await comfyPage.vueNodes.enterSubgraph('2')
const ksampler = comfyPage.vueNodes.getNodeByTitle('KSampler')
await comfyPage.subgraph.promoteWidget(ksampler, 'cfg')
const fromSlot = await comfyPage.vueNodes.getSlot('steps', ksampler)
const toPos = await comfyPage.subgraph.getInputSlot().getOpenSlotPosition()
await fromSlot.dragTo(comfyPage.canvas, { targetPosition: toPos })
const isConnected = () => comfyPage.vueNodes.isSlotConnected(fromSlot)
await expect.poll(isConnected).toBe(true)
await comfyPage.subgraph.exitViaBreadcrumb()
})
await expect(
subgraphNode.locator('.lg-node-widget').first(),
'linked widgets are first by default'
).toHaveText('steps')
const { editor } = comfyPage.subgraph
await editor.open(subgraphNode)
const stepsItem = editor.resolvePromotionItem({ widgetName: 'steps' })
const cfgItem = editor.resolvePromotionItem({ widgetName: 'cfg' })
await comfyMouse.move(await comfyMouse.getCenter(stepsItem))
await comfyMouse.down()
const { x, y, width, height } = (await cfgItem.boundingBox())!
await comfyMouse.move({ x: x + width / 2, y: y + height })
await comfyMouse.up()
const firstItem = editor.resolvePromotionItem({ widgetName: '' }).first()
await expect(firstItem, 'Swap widget order').toContainText('cfg')
// TODO: fix actual bug.
await expect(
subgraphNode.locator('.lg-node-widget').first(),
'Linked widget is first on node'
).not.toHaveText('cfg')
})