mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 01:07:56 +00:00
## Summary Right-clicking a bypassed node showed two bypass-related items in the Vue "More Options" context menu (FE-720): - Plain `Bypass` from the legacy LiteGraph `getExtraMenuOptions` hook in `litegraphService.ts` - `Remove Bypass` (with `Ctrl+B` and an icon) from the Vue `getBypassOption` composable The Vue menu's exact-label deduplicator in `contextMenuConverter.ts` collapsed the unbypassed case (both emit `Bypass` → Vue source wins) but not the bypassed case (`Bypass` vs `Remove Bypass`), so the duplicate leaked through whenever the node was bypassed. ### before <img width="1920" height="958" alt="fe-720-before" src="https://github.com/user-attachments/assets/ef001aca-d70e-4798-ac61-01cc34c31e44" /> ### after <img width="1920" height="958" alt="fe-720-after" src="https://github.com/user-attachments/assets/d6d2bf4b-cb98-4b30-9dac-9bd4b68a7e36" /> #### single active node (KSampler) <img width="1920" height="958" alt="fe-720-1-unbypassed-node-menu" src="https://github.com/user-attachments/assets/bec9cd47-2f2d-4adb-b95b-266e7969a36c" /> #### single bypassed node (Load Checkpoint) <img width="1920" height="958" alt="fe-720-2-bypassed-node-menu" src="https://github.com/user-attachments/assets/91f80157-836d-4fce-adad-474f31baff04" /> #### KSampler + bypassed Load Checkpoint <img width="1920" height="958" alt="fe-720-3-mixed-selection-menu" src="https://github.com/user-attachments/assets/e4780b16-08e5-4f87-80e9-3ff65a5acdae" /> ## Root cause `src/services/litegraphService.ts` pushes a `Bypass` entry from its legacy `getExtraMenuOptions` hook in addition to the Vue `getBypassOption`. In Vue-menu mode both reach the menu; the exact-label dedup in `contextMenuConverter.ts` only collapses them when the labels match, which fails once the node is bypassed and the Vue side switches to `Remove Bypass`. ## Fix Add `Bypass` and `Remove Bypass` to the `HARD_BLACKLIST` in `contextMenuConverter.ts`. The blacklist filters the legacy emission out of the Vue conversion pipeline (`convertContextMenuToOptions`) before it is ever merged, so Vue's `getBypassOption` is the single source of the bypass item in every node state — no duplicate is created in the first place. This is the established convention for legacy items that the Vue menu replaces (`Properties`, `Colors`, `Shapes`, `Title`, `Mode`, `Properties Panel`, `Copy (Clipspace)`); Bypass is the same category. `litegraphService.ts` reverts to a plain `content: 'Bypass'` and no longer imports `areAllSelectedNodesInMode` or i18n keys for this entry. The Vue `getBypassOption` label is still derived from the same selection-aware predicate (`areAllSelectedNodesInMode`) that `toggleSelectedNodesMode` uses, so on mixed selections the label stays in sync with the action — it shows `Bypass` when clicking would bypass the rest, rather than `Remove Bypass`. **Trade-off:** the classic LiteGraph canvas menu (`Comfy.VueNodes.Enabled: false`) renders `litegraphService`'s options directly without going through `convertContextMenuToOptions`, so it shows a plain `Bypass` regardless of node state. This matches the pre-PR behavior (the legacy push was already a hardcoded `Bypass`), so it is not a regression. ## Considered and rejected - **`equivalents` map** (`bypass: ['bypass', 'remove bypass']`) — would collapse `Bypass` and `Remove Bypass` as synonyms, which is semantically wrong: they are distinct actions that must stay distinguishable, and the rule would also misfire on the unbypassed case. A converter test locks in that they are not treated as equivalents. - **State-aware label on the legacy push** (matching the Vue label so the exact-label dedup collapses them) — works, and additionally gives the classic canvas menu a state-aware label, but it couples `litegraphService` to the selection predicate and i18n keys solely to keep a downstream dedup load-bearing. `HARD_BLACKLIST` removes the duplicate at the source instead of creating, converting, then collapsing it. The only thing lost is the classic-menu state-aware label, which was never present pre-PR. - **Gating the legacy push on `Comfy.UseNewMenu === 'Disabled'`** — the setting that selects the legacy vs Vue context menu is `Comfy.VueNodes.Enabled`, not `Comfy.UseNewMenu` (an unrelated top-menu-bar toggle). Gating on `UseNewMenu` would drop the Bypass entry from the legacy canvas menu for the OSS default (`VueNodes.Enabled: false` + `UseNewMenu: 'Top'`). - **Suppressing the legacy callback via `SUPPRESSED_LITEGRAPH_CALLBACKS`** — matches by callback identity and adds cross-file coupling for what is a simple label-based filter that `HARD_BLACKLIST` already expresses. ## Cleanups (review feedback) - Removed the now-dead `NodeSelectionState.bypassed` field and its producer (no consumers after the label switch). - Replaced the `vue-i18n` mock in `useNodeMenuOptions.test.ts` with a real `createI18n` instance per `docs/testing/vitest-patterns.md`; removed a `ts-expect-error` via a typed hoisted `app` mock. - Simplified `getSelectedNodeArray` to `Object.values(app.canvas.selected_nodes ?? {})`. ## Tests - `useSelectedLiteGraphItems.test.ts` — `areAllSelectedNodesInMode`: all-bypassed → true, mixed → false, empty → false. - `useNodeMenuOptions.test.ts` — Vue label is `Bypass` (active / mixed) and `Remove Bypass` (all bypassed). - `contextMenuConverter.test.ts` — the legacy `Bypass` push is filtered by `HARD_BLACKLIST` so the Vue item is the only bypass entry (keeps shortcut/source); `Bypass` and `Remove Bypass` are not treated as label equivalents. - `browser_tests/tests/vueNodes/interactions/node/contextMenu.spec.ts` — e2e regression: exactly one bypass-family item per node state. Verified live on a bypassed Load Checkpoint: single `Remove Bypass` → toggle un-bypasses → single `Bypass`; no duplicate, rest of the menu intact. - Fixes FE-720 --------- Co-authored-by: Alexander Brown <drjkl@comfy.org>
548 lines
18 KiB
TypeScript
548 lines
18 KiB
TypeScript
import type { Locator } from '@playwright/test'
|
|
|
|
import {
|
|
comfyExpect as expect,
|
|
comfyPageFixture as test
|
|
} from '@e2e/fixtures/ComfyPage'
|
|
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
|
import { TestIds } from '@e2e/fixtures/selectors'
|
|
|
|
const BYPASS_CLASS = /before:bg-bypass\/60/
|
|
|
|
async function clickExactMenuItem(comfyPage: ComfyPage, name: string) {
|
|
await comfyPage.contextMenu.clickMenuItemExact(name)
|
|
await expect(comfyPage.contextMenu.primeVueMenu).toBeHidden()
|
|
}
|
|
|
|
async function openContextMenu(comfyPage: ComfyPage, nodeTitle: string) {
|
|
const fixture = await comfyPage.vueNodes.getFixtureByTitle(nodeTitle)
|
|
await comfyPage.contextMenu.openForVueNode(fixture.header)
|
|
return comfyPage.contextMenu.primeVueMenu
|
|
}
|
|
|
|
async function openMultiNodeContextMenu(
|
|
comfyPage: ComfyPage,
|
|
titles: string[]
|
|
) {
|
|
// deselectAll via evaluate — clearSelection() clicks at a fixed position
|
|
// which can hit nodes or the toolbar overlay
|
|
await comfyPage.page.evaluate(() => window.app!.canvas.deselectAll())
|
|
await comfyPage.nextFrame()
|
|
|
|
for (const title of titles) {
|
|
const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
|
|
await fixture.header.click({ modifiers: ['ControlOrMeta'] })
|
|
}
|
|
await comfyPage.nextFrame()
|
|
|
|
const firstFixture = await comfyPage.vueNodes.getFixtureByTitle(titles[0])
|
|
const box = await firstFixture.header.boundingBox()
|
|
if (!box) throw new Error(`Header for "${titles[0]}" not found`)
|
|
await comfyPage.page.mouse.click(
|
|
box.x + box.width / 2,
|
|
box.y + box.height / 2,
|
|
{ button: 'right' }
|
|
)
|
|
|
|
const menu = comfyPage.contextMenu.primeVueMenu
|
|
await menu.waitFor({ state: 'visible' })
|
|
return menu
|
|
}
|
|
|
|
function getNodeWrapper(comfyPage: ComfyPage, nodeTitle: string): Locator {
|
|
return comfyPage.vueNodes
|
|
.getNodeByTitle(nodeTitle)
|
|
.getByTestId(TestIds.node.innerWrapper)
|
|
}
|
|
|
|
async function getNodeRef(comfyPage: ComfyPage, nodeTitle: string) {
|
|
const refs = await comfyPage.nodeOps.getNodeRefsByTitle(nodeTitle)
|
|
return refs[0]
|
|
}
|
|
|
|
test.describe('Vue Node Context Menu', { tag: '@vue-nodes' }, () => {
|
|
test.describe('Single Node Actions', () => {
|
|
test('should rename node via context menu', async ({ comfyPage }) => {
|
|
await openContextMenu(comfyPage, 'KSampler')
|
|
await clickExactMenuItem(comfyPage, 'Rename')
|
|
|
|
await comfyPage.titleEditor.expectVisible()
|
|
await comfyPage.titleEditor.setTitle('My Renamed Sampler')
|
|
await comfyPage.nextFrame()
|
|
|
|
const renamedNode =
|
|
comfyPage.vueNodes.getNodeByTitle('My Renamed Sampler')
|
|
await expect(renamedNode).toBeVisible()
|
|
})
|
|
|
|
test('should open node info in the right side panel via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.settings.setSetting('Comfy.RightSidePanel.IsOpen', false)
|
|
await expect(comfyPage.menu.propertiesPanel.root).toBeHidden()
|
|
|
|
await openContextMenu(comfyPage, 'KSampler')
|
|
await clickExactMenuItem(comfyPage, 'Node Info')
|
|
|
|
const panel = comfyPage.menu.propertiesPanel.root
|
|
await expect(panel).toBeVisible()
|
|
await expect(panel.getByTestId('panel-tab-info')).toHaveAttribute(
|
|
'aria-selected',
|
|
'true'
|
|
)
|
|
await expect(comfyPage.menu.nodeLibraryTab.selectedTabButton).toBeHidden()
|
|
})
|
|
|
|
test('should copy and paste node via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
|
|
|
await openContextMenu(comfyPage, 'Load Checkpoint')
|
|
await clickExactMenuItem(comfyPage, 'Copy')
|
|
|
|
// Internal clipboard paste (menu Copy uses canvas clipboard, not OS)
|
|
await comfyPage.page.evaluate(() => {
|
|
window.app!.canvas.pasteFromClipboard({ connectInputs: false })
|
|
})
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
|
.toBe(initialCount + 1)
|
|
})
|
|
|
|
test('should duplicate node via context menu', async ({ comfyPage }) => {
|
|
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
|
|
|
await openContextMenu(comfyPage, 'Load Checkpoint')
|
|
await clickExactMenuItem(comfyPage, 'Duplicate')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
|
.toBe(initialCount + 1)
|
|
})
|
|
|
|
test('should pin and unpin node via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const nodeTitle = 'Load Checkpoint'
|
|
const nodeRef = await getNodeRef(comfyPage, nodeTitle)
|
|
|
|
// Pin via context menu
|
|
await openContextMenu(comfyPage, nodeTitle)
|
|
await clickExactMenuItem(comfyPage, 'Pin')
|
|
|
|
const fixture = await comfyPage.vueNodes.getFixtureByTitle(nodeTitle)
|
|
await expect(fixture.pinIndicator).toBeVisible()
|
|
await expect.poll(() => nodeRef.isPinned()).toBe(true)
|
|
|
|
// Verify drag blocked
|
|
const header = fixture.header
|
|
const posBeforeDrag = await header.boundingBox()
|
|
if (!posBeforeDrag) throw new Error('Header not found')
|
|
await comfyPage.canvasOps.dragAndDrop(
|
|
{ x: posBeforeDrag.x + 10, y: posBeforeDrag.y + 10 },
|
|
{ x: posBeforeDrag.x + 256, y: posBeforeDrag.y + 256 }
|
|
)
|
|
await expect
|
|
.poll(async () => await header.boundingBox())
|
|
.toEqual(posBeforeDrag)
|
|
|
|
// Unpin via context menu
|
|
await openContextMenu(comfyPage, nodeTitle)
|
|
await clickExactMenuItem(comfyPage, 'Unpin')
|
|
|
|
await expect(fixture.pinIndicator).toBeHidden()
|
|
await expect.poll(() => nodeRef.isPinned()).toBe(false)
|
|
})
|
|
|
|
test('should bypass node and remove bypass via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const nodeTitle = 'Load Checkpoint'
|
|
const nodeRef = await getNodeRef(comfyPage, nodeTitle)
|
|
|
|
await openContextMenu(comfyPage, nodeTitle)
|
|
await clickExactMenuItem(comfyPage, 'Bypass')
|
|
|
|
await expect(nodeRef).toBeBypassed()
|
|
await expect(getNodeWrapper(comfyPage, nodeTitle)).toHaveClass(
|
|
BYPASS_CLASS
|
|
)
|
|
|
|
await openContextMenu(comfyPage, nodeTitle)
|
|
await clickExactMenuItem(comfyPage, 'Remove Bypass')
|
|
|
|
await expect(nodeRef).not.toBeBypassed()
|
|
await expect(getNodeWrapper(comfyPage, nodeTitle)).not.toHaveClass(
|
|
BYPASS_CLASS
|
|
)
|
|
})
|
|
|
|
test('shows exactly one bypass menu item per state (FE-720 regression)', async ({
|
|
comfyPage
|
|
}) => {
|
|
const nodeTitle = 'Load Checkpoint'
|
|
const nodeRef = await getNodeRef(comfyPage, nodeTitle)
|
|
const bypassItem = comfyPage.contextMenu.menuItem('Bypass')
|
|
const removeBypassItem = comfyPage.contextMenu.menuItem('Remove Bypass')
|
|
|
|
await openContextMenu(comfyPage, nodeTitle)
|
|
await expect(bypassItem).toHaveCount(1)
|
|
await expect(removeBypassItem).toHaveCount(0)
|
|
await clickExactMenuItem(comfyPage, 'Bypass')
|
|
await expect(nodeRef).toBeBypassed()
|
|
|
|
await openContextMenu(comfyPage, nodeTitle)
|
|
await expect(removeBypassItem).toHaveCount(1)
|
|
await expect(bypassItem).toHaveCount(0)
|
|
await clickExactMenuItem(comfyPage, 'Remove Bypass')
|
|
await expect(nodeRef).not.toBeBypassed()
|
|
})
|
|
|
|
test('should minimize and expand node via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const fixture = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
|
await expect(fixture.body).toBeVisible()
|
|
|
|
await openContextMenu(comfyPage, 'KSampler')
|
|
await clickExactMenuItem(comfyPage, 'Minimize Node')
|
|
await expect(fixture.body).toBeHidden()
|
|
|
|
await openContextMenu(comfyPage, 'KSampler')
|
|
await clickExactMenuItem(comfyPage, 'Expand Node')
|
|
await expect(fixture.body).toBeVisible()
|
|
})
|
|
|
|
test('should convert node to subgraph via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
await openContextMenu(comfyPage, 'KSampler')
|
|
await clickExactMenuItem(comfyPage, 'Convert to Subgraph')
|
|
|
|
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
|
await expect(subgraphNode).toBeVisible()
|
|
|
|
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
|
|
})
|
|
})
|
|
|
|
test.describe('Image Node Actions', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.page
|
|
.context()
|
|
.grantPermissions(['clipboard-read', 'clipboard-write'])
|
|
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
|
await comfyPage.vueNodes.waitForNodes(1)
|
|
await comfyPage.page
|
|
.locator('[data-node-id] img')
|
|
.first()
|
|
.waitFor({ state: 'visible' })
|
|
|
|
const [loadImageNode] =
|
|
await comfyPage.nodeOps.getNodeRefsByTitle('Load Image')
|
|
if (!loadImageNode) throw new Error('Load Image node not found')
|
|
|
|
await expect
|
|
.poll(() =>
|
|
comfyPage.page.evaluate(
|
|
(nodeId) =>
|
|
window.app!.graph.getNodeById(nodeId)?.imgs?.length ?? 0,
|
|
loadImageNode.id
|
|
)
|
|
)
|
|
.toBeGreaterThan(0)
|
|
})
|
|
|
|
test('should copy image to clipboard via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
await openContextMenu(comfyPage, 'Load Image')
|
|
await clickExactMenuItem(comfyPage, 'Copy Image')
|
|
|
|
// Verify the clipboard contains an image
|
|
await expect
|
|
.poll(async () => {
|
|
return comfyPage.page.evaluate(async () => {
|
|
const items = await navigator.clipboard.read()
|
|
return items.some((item) =>
|
|
item.types.some((t) => t.startsWith('image/'))
|
|
)
|
|
})
|
|
})
|
|
.toBe(true)
|
|
})
|
|
|
|
test('should paste image to LoadImage node via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Capture the original image src from the node's preview
|
|
const imagePreview = comfyPage.vueNodes
|
|
.getNodeByTitle('Load Image')
|
|
.getByTestId(TestIds.node.mainImage)
|
|
const originalSrc = await imagePreview.getAttribute('src')
|
|
|
|
// Write a test image into the browser clipboard
|
|
await comfyPage.page.evaluate(async () => {
|
|
const resp = await fetch('/api/view?filename=example.png&type=input')
|
|
const blob = await resp.blob()
|
|
await navigator.clipboard.write([
|
|
new ClipboardItem({ [blob.type]: blob })
|
|
])
|
|
})
|
|
|
|
// Right-click and select Paste Image
|
|
await openContextMenu(comfyPage, 'Load Image')
|
|
await clickExactMenuItem(comfyPage, 'Paste Image')
|
|
|
|
// Verify the image preview src changed
|
|
await expect(imagePreview).not.toHaveAttribute('src', originalSrc!)
|
|
})
|
|
|
|
test('should open image in new tab via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
await openContextMenu(comfyPage, 'Load Image')
|
|
|
|
const popupPromise = comfyPage.page.waitForEvent('popup')
|
|
await clickExactMenuItem(comfyPage, 'Open Image')
|
|
const popup = await popupPromise
|
|
|
|
expect(popup.url()).toContain('/api/view')
|
|
expect(popup.url()).toContain('filename=')
|
|
await popup.close()
|
|
})
|
|
|
|
test('should download image via Save Image context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
await openContextMenu(comfyPage, 'Load Image')
|
|
|
|
const downloadPromise = comfyPage.page.waitForEvent('download')
|
|
await clickExactMenuItem(comfyPage, 'Save Image')
|
|
const download = await downloadPromise
|
|
|
|
expect(download.suggestedFilename()).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
test.describe('Subgraph Actions', () => {
|
|
test('should convert to subgraph and unpack back', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Convert KSampler to subgraph
|
|
await openContextMenu(comfyPage, 'KSampler')
|
|
await clickExactMenuItem(comfyPage, 'Convert to Subgraph')
|
|
|
|
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
|
await expect(subgraphNode).toBeVisible()
|
|
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeHidden()
|
|
|
|
// Unpack the subgraph
|
|
await openContextMenu(comfyPage, 'New Subgraph')
|
|
await clickExactMenuItem(comfyPage, 'Unpack Subgraph')
|
|
|
|
await expect(comfyPage.vueNodes.getNodeByTitle('KSampler')).toBeVisible()
|
|
await expect(
|
|
comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
|
).toBeHidden()
|
|
})
|
|
|
|
test('should open properties panel via Edit Subgraph Widgets', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Convert to subgraph first
|
|
await openContextMenu(comfyPage, 'Empty Latent Image')
|
|
await clickExactMenuItem(comfyPage, 'Convert to Subgraph')
|
|
await expect(
|
|
comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
|
).toBeVisible()
|
|
|
|
// Right-click subgraph and edit widgets
|
|
await openContextMenu(comfyPage, 'New Subgraph')
|
|
await clickExactMenuItem(comfyPage, 'Edit Subgraph Widgets')
|
|
|
|
await expect(comfyPage.page.getByTestId('properties-panel')).toBeVisible()
|
|
})
|
|
|
|
test('should add subgraph to library and find in node library', async ({
|
|
comfyPage
|
|
}) => {
|
|
// Convert to subgraph first
|
|
await openContextMenu(comfyPage, 'KSampler')
|
|
await clickExactMenuItem(comfyPage, 'Convert to Subgraph')
|
|
await expect(
|
|
comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
|
).toBeVisible()
|
|
|
|
// Add to library
|
|
await openContextMenu(comfyPage, 'New Subgraph')
|
|
await clickExactMenuItem(comfyPage, 'Add Subgraph to Library')
|
|
|
|
// Fill the blueprint name
|
|
await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'visible' })
|
|
await comfyPage.nodeOps.fillPromptDialog('TestBlueprint')
|
|
|
|
// Open node library sidebar and search for the blueprint
|
|
await comfyPage.menu.nodeLibraryTab.tabButton.click()
|
|
const searchBox = comfyPage.page.getByRole('combobox', {
|
|
name: 'Search'
|
|
})
|
|
await searchBox.waitFor({ state: 'visible' })
|
|
await searchBox.fill('TestBlueprint')
|
|
|
|
await expect(comfyPage.page.getByText('TestBlueprint')).toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('Multi-Node Actions', () => {
|
|
const nodeTitles = ['Load Checkpoint', 'KSampler']
|
|
|
|
test('should batch rename selected nodes via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Rename')
|
|
|
|
await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'visible' })
|
|
await comfyPage.nodeOps.fillPromptDialog('MyNode')
|
|
|
|
await expect(comfyPage.vueNodes.getNodeByTitle('MyNode 1')).toBeVisible()
|
|
await expect(comfyPage.vueNodes.getNodeByTitle('MyNode 2')).toBeVisible()
|
|
})
|
|
|
|
test('should copy and paste selected nodes via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
|
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Copy')
|
|
|
|
await comfyPage.page.evaluate(() => {
|
|
window.app!.canvas.pasteFromClipboard({ connectInputs: false })
|
|
})
|
|
await comfyPage.nextFrame()
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
|
.toBe(initialCount + nodeTitles.length)
|
|
})
|
|
|
|
test('should duplicate selected nodes via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
|
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Duplicate')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
|
.toBe(initialCount + nodeTitles.length)
|
|
})
|
|
|
|
test('should pin and unpin selected nodes via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Pin')
|
|
|
|
for (const title of nodeTitles) {
|
|
const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
|
|
await expect(fixture.pinIndicator).toBeVisible()
|
|
}
|
|
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Unpin')
|
|
|
|
for (const title of nodeTitles) {
|
|
const fixture = await comfyPage.vueNodes.getFixtureByTitle(title)
|
|
await expect(fixture.pinIndicator).toBeHidden()
|
|
}
|
|
})
|
|
|
|
test('should bypass and remove bypass on selected nodes via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Bypass')
|
|
|
|
for (const title of nodeTitles) {
|
|
const nodeRef = await getNodeRef(comfyPage, title)
|
|
await expect(nodeRef).toBeBypassed()
|
|
await expect(getNodeWrapper(comfyPage, title)).toHaveClass(BYPASS_CLASS)
|
|
}
|
|
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Remove Bypass')
|
|
|
|
for (const title of nodeTitles) {
|
|
const nodeRef = await getNodeRef(comfyPage, title)
|
|
await expect(nodeRef).not.toBeBypassed()
|
|
await expect(getNodeWrapper(comfyPage, title)).not.toHaveClass(
|
|
BYPASS_CLASS
|
|
)
|
|
}
|
|
})
|
|
|
|
test('should minimize and expand selected nodes via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const fixture1 =
|
|
await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
|
|
const fixture2 = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
|
|
|
await expect(fixture1.body).toBeVisible()
|
|
await expect(fixture2.body).toBeVisible()
|
|
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Minimize Node')
|
|
|
|
await expect(fixture1.body).toBeHidden()
|
|
await expect(fixture2.body).toBeHidden()
|
|
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Expand Node')
|
|
|
|
await expect(fixture1.body).toBeVisible()
|
|
await expect(fixture2.body).toBeVisible()
|
|
})
|
|
|
|
test('should frame selected nodes via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const initialGroupCount = await comfyPage.page.evaluate(
|
|
() => window.app!.graph.groups.length
|
|
)
|
|
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Frame Nodes')
|
|
|
|
await expect
|
|
.poll(() =>
|
|
comfyPage.page.evaluate(() => window.app!.graph.groups.length)
|
|
)
|
|
.toBe(initialGroupCount + 1)
|
|
})
|
|
|
|
test('should convert selected nodes to subgraph via context menu', async ({
|
|
comfyPage
|
|
}) => {
|
|
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
|
|
|
await openMultiNodeContextMenu(comfyPage, nodeTitles)
|
|
await clickExactMenuItem(comfyPage, 'Convert to Subgraph')
|
|
|
|
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
|
|
await expect(subgraphNode).toBeVisible()
|
|
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
|
.toBe(initialCount - nodeTitles.length + 1)
|
|
})
|
|
})
|
|
})
|