mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-15 11:44:10 +00:00
## Summary https://linear.app/comfyorg/issue/FE-867/bug-node-expand-menu-doesnt-work-nodes-immediately-collapse-after Recreates #12175 on a fresh `main` base (original branch's CI failed only because its `frontend-dist` artifact had expired — not a code issue). Original work by @christian-byrne / Glary-Bot, cherry-picked here so it can land while he's offline. The Vue right-click "More Options" node menu shows duplicates for collapse/expand functionality: - **Vue source**: `Minimize Node` / `Expand Node` (works) - **LiteGraph source**: `Resize`, `Collapse`, `Expand` (silently no-op in this menu — the converter wrapper invokes `LGraphCanvas.onMenuNodeCollapse` without the `node` arg it expects) Suppress the LiteGraph duplicates in `convertContextMenuToOptions` by matching the built-in **callback identity** (`LGraphCanvas.onMenuResizeNode`, `LGraphCanvas.onMenuNodeCollapse`), not the raw label. Matching by identity avoids accidentally hiding extension-provided items that share those labels. Also align `CORE_MENU_ITEMS` / `MENU_ORDER` on the Vue label `Expand Node` so the toggled Minimize/Expand pair sorts correctly. ## Scope of suppression Only the Vue node menu (via `convertContextMenuToOptions`) is affected. The raw `LGraphCanvas.getNodeMenuOptions` output is untouched, so: - The legacy right-click menu (`Comfy.UseNewMenu` disabled) still has `Collapse` / `Resize`. - `useLoad3d.ts`, which calls `new LiteGraph.ContextMenu(app.canvas.getNodeMenuOptions(node), ...)`, is unaffected. - Extensions that monkey-patch `getNodeMenuOptions` continue to receive the full option list. ## Tests - `contextMenuConverter.test.ts`: covers both that built-in entries are dropped by identity AND that extension-provided items with the same labels survive. - E2E `selectionToolboxMoreActions.spec.ts`: asserts the Vue "More Options" menu shows `Minimize Node` but no `Resize`/`Collapse`/`Expand`. - `pnpm typecheck` clean. Supersedes #12175. --------- Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
import {
|
|
comfyExpect as expect,
|
|
comfyPageFixture as test
|
|
} from '@e2e/fixtures/ComfyPage'
|
|
import { openMoreOptions } from '@e2e/fixtures/utils/selectionToolbox'
|
|
|
|
test.describe('Selection Toolbox - More Options', { tag: '@ui' }, () => {
|
|
test.describe('Single node actions', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
|
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
|
await comfyPage.nextFrame()
|
|
})
|
|
|
|
test('pin and unpin node via More Options menu', async ({ comfyPage }) => {
|
|
const nodeRef = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
|
)[0]
|
|
await comfyPage.nodeOps.selectNodeWithPan(nodeRef)
|
|
|
|
await expect(nodeRef).not.toBePinned()
|
|
|
|
let menu = await openMoreOptions(comfyPage)
|
|
await menu.getByText('Pin', { exact: true }).click()
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect(nodeRef).toBePinned()
|
|
|
|
menu = await openMoreOptions(comfyPage)
|
|
await menu.getByText('Unpin', { exact: true }).click()
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect(nodeRef).not.toBePinned()
|
|
})
|
|
|
|
test('minimize and expand node via More Options menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const nodeRef = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
|
)[0]
|
|
await comfyPage.nodeOps.selectNodeWithPan(nodeRef)
|
|
|
|
await expect(nodeRef).not.toBeCollapsed()
|
|
|
|
let menu = await openMoreOptions(comfyPage)
|
|
await menu.getByText('Minimize Node', { exact: true }).click()
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect(nodeRef).toBeCollapsed()
|
|
|
|
menu = await openMoreOptions(comfyPage)
|
|
await menu.getByText('Expand Node', { exact: true }).click()
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect(nodeRef).not.toBeCollapsed()
|
|
})
|
|
|
|
test('More Options menu does not surface duplicate LiteGraph Resize / Collapse / Expand entries', async ({
|
|
comfyPage
|
|
}) => {
|
|
const nodeRef = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
|
)[0]
|
|
await comfyPage.nodeOps.selectNodeWithPan(nodeRef)
|
|
|
|
const menu = await openMoreOptions(comfyPage)
|
|
|
|
await expect(
|
|
menu.getByText('Minimize Node', { exact: true })
|
|
).toBeVisible()
|
|
await expect(
|
|
menu.getByRole('menuitem', { name: 'Resize', exact: true })
|
|
).toHaveCount(0)
|
|
await expect(
|
|
menu.getByRole('menuitem', { name: 'Collapse', exact: true })
|
|
).toHaveCount(0)
|
|
|
|
await menu.getByText('Minimize Node', { exact: true }).click()
|
|
await openMoreOptions(comfyPage)
|
|
|
|
await expect(
|
|
menu.getByRole('menuitem', { name: 'Expand', exact: true })
|
|
).toHaveCount(0)
|
|
})
|
|
|
|
test('copy via More Options menu', async ({ comfyPage }) => {
|
|
const nodeRef = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
|
)[0]
|
|
await comfyPage.nodeOps.selectNodeWithPan(nodeRef)
|
|
|
|
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
|
|
|
const menu = await openMoreOptions(comfyPage)
|
|
await menu.getByText('Copy', { exact: true }).click()
|
|
await comfyPage.nextFrame()
|
|
|
|
// Paste the copied node
|
|
await comfyPage.clipboard.paste()
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
|
.toBe(initialCount + 1)
|
|
})
|
|
|
|
test('duplicate via More Options menu', async ({ comfyPage }) => {
|
|
const nodeRef = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
|
)[0]
|
|
await comfyPage.nodeOps.selectNodeWithPan(nodeRef)
|
|
|
|
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
|
|
|
const menu = await openMoreOptions(comfyPage)
|
|
await menu.getByText('Duplicate', { exact: true }).click()
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
|
.toBe(initialCount + 1)
|
|
})
|
|
|
|
test('refresh button is rendered in toolbox when node is selected', async ({
|
|
comfyPage
|
|
}) => {
|
|
const nodeRef = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
|
)[0]
|
|
await comfyPage.nodeOps.selectNodeWithPan(nodeRef)
|
|
|
|
await expect(comfyPage.selectionToolbox).toBeVisible()
|
|
|
|
// The refresh button uses v-show so it's always in the DOM;
|
|
// actual visibility depends on backend-provided widget refresh
|
|
// capabilities which vary between local and CI environments.
|
|
const refreshButton = comfyPage.page.getByTestId('refresh-button')
|
|
await expect(refreshButton).toBeAttached()
|
|
})
|
|
})
|
|
|
|
test.describe('Multiple node actions', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
|
await comfyPage.workflow.loadWorkflow('default')
|
|
await comfyPage.nextFrame()
|
|
})
|
|
|
|
test('bypass button toggles bypass on multiple selected nodes', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.nodeOps.selectNodes(['KSampler', 'Empty Latent Image'])
|
|
await comfyPage.nextFrame()
|
|
|
|
const ksampler = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
|
|
)[0]
|
|
const emptyLatent = (
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('Empty Latent Image')
|
|
)[0]
|
|
|
|
await expect(ksampler).not.toBeBypassed()
|
|
await expect(emptyLatent).not.toBeBypassed()
|
|
|
|
const bypassButton = comfyPage.page.getByTestId('bypass-button')
|
|
await expect(bypassButton).toBeVisible()
|
|
await bypassButton.click()
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect(ksampler).toBeBypassed()
|
|
await expect(emptyLatent).toBeBypassed()
|
|
|
|
// Toggle back
|
|
await bypassButton.click()
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect(ksampler).not.toBeBypassed()
|
|
await expect(emptyLatent).not.toBeBypassed()
|
|
})
|
|
})
|
|
})
|