diff --git a/browser_tests/fixtures/ComfyMouse.ts b/browser_tests/fixtures/ComfyMouse.ts index 0b3b122928..616d97ff3d 100644 --- a/browser_tests/fixtures/ComfyMouse.ts +++ b/browser_tests/fixtures/ComfyMouse.ts @@ -66,6 +66,12 @@ export class ComfyMouse implements Omit { await this.drop(options) } + async getCenter(locator: Locator): Promise { + 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) diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index 1f5e58febc..58413e010f 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -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) + } } diff --git a/browser_tests/fixtures/components/SubgraphEditor.ts b/browser_tests/fixtures/components/SubgraphEditor.ts new file mode 100644 index 0000000000..64326702c3 --- /dev/null +++ b/browser_tests/fixtures/components/SubgraphEditor.ts @@ -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) + } +} diff --git a/browser_tests/fixtures/helpers/SubgraphHelper.ts b/browser_tests/fixtures/helpers/SubgraphHelper.ts index e09859e73b..ad7fd3022e 100644 --- a/browser_tests/fixtures/helpers/SubgraphHelper.ts +++ b/browser_tests/fixtures/helpers/SubgraphHelper.ts @@ -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 { const widget = nodeLocator.getByLabel(widgetName, { exact: true }) await this.comfyPage.contextMenu diff --git a/browser_tests/tests/subgraph/subgraphPromotion.spec.ts b/browser_tests/tests/subgraph/subgraphPromotion.spec.ts index 99c62666ef..ac3ea54c69 100644 --- a/browser_tests/tests/subgraph/subgraphPromotion.spec.ts +++ b/browser_tests/tests/subgraph/subgraphPromotion.spec.ts @@ -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') +})