Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { VueNodeHelpers } from './VueNodeHelpers'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { Minimap } from './components/Minimap'
import { SettingDialog } from './components/SettingDialog'
import {
NodeLibrarySidebarTab,
Expand All @@ -33,6 +34,7 @@ class ComfyMenu {
private _workflowsTab: WorkflowsSidebarTab | null = null
private _queueTab: QueueSidebarTab | null = null
private _topbar: Topbar | null = null
private _minimap: Minimap | null = null

public readonly sideToolbar: Locator
public readonly themeToggleButton: Locator
Expand Down Expand Up @@ -70,6 +72,11 @@ class ComfyMenu {
return this._topbar
}

get minimap() {
this._minimap ??= new Minimap(this.page)
return this._minimap
}

async toggleTheme() {
await this.themeToggleButton.click()
await this.page.evaluate(() => {
Expand Down
41 changes: 41 additions & 0 deletions browser_tests/fixtures/components/Minimap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Locator, Page } from '@playwright/test'

export class Minimap {
constructor(public readonly page: Page) {}

get mainContainer(): Locator {
return this.page.locator('.minimap-main-container')
}

get container(): Locator {
return this.page.locator('.litegraph-minimap')
}

get canvas(): Locator {
return this.container.locator('.minimap-canvas')
}

get viewport(): Locator {
return this.container.locator('.minimap-viewport')
}

get settingsButton(): Locator {
return this.container.getByRole('button').first()
}

get closeButton(): Locator {
return this.container.getByTestId('close-minmap-button')
}

async clickCanvas(options?: Parameters<Locator['click']>[0]): Promise<void> {
await this.canvas.click(options)
}

async clickSettingsButton(): Promise<void> {
await this.settingsButton.click()
}

async close(): Promise<void> {
await this.closeButton.click()
}
}
31 changes: 27 additions & 4 deletions browser_tests/tests/copyPaste.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { expect } from '@playwright/test'

import { comfyPageFixture as test } from '../fixtures/ComfyPage'

test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})

test.describe('Copy Paste', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})

test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.page.mouse.move(10, 10)
Expand All @@ -15,6 +15,29 @@ test.describe('Copy Paste', () => {
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})

test('Can copy and paste node after clicking minimap', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Minimap.Visible', true)

const latentNodeTitle = 'Empty Latent Image'
const initialLatentNodeCt =
await comfyPage.getNodeRefsByTitle(latentNodeTitle)

await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()

// Click minimap to lose focus.
await comfyPage.menu.minimap.clickCanvas({ force: true })

// Paste node.
await comfyPage.ctrlV()
const expectedNodeCt = initialLatentNodeCt.length + 1
const latentNodeCt = await comfyPage.getNodeRefsByTitle(latentNodeTitle)
expect(latentNodeCt.length).toBe(expectedNodeCt)
})

test('Can copy and paste node with link', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
await comfyPage.page.mouse.move(10, 10)
Expand Down
57 changes: 31 additions & 26 deletions browser_tests/tests/minimap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,65 +14,70 @@ test.describe('Minimap', () => {
})

test('Validate minimap is visible by default', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
const minimap = comfyPage.menu.minimap

await expect(minimapContainer).toBeVisible()

const minimapCanvas = minimapContainer.locator('.minimap-canvas')
await expect(minimapCanvas).toBeVisible()

const minimapViewport = minimapContainer.locator('.minimap-viewport')
await expect(minimapViewport).toBeVisible()

await expect(minimapContainer).toHaveCSS('position', 'relative')
await expect(minimap.container).toBeVisible()
await expect(minimap.canvas).toBeVisible()
await expect(minimap.viewport).toBeVisible()
await expect(minimap.container).toHaveCSS('position', 'relative')

// position and z-index validation moved to the parent container of the minimap
const minimapMainContainer = comfyPage.page.locator(
'.minimap-main-container'
)
await expect(minimapMainContainer).toHaveCSS('position', 'absolute')
await expect(minimapMainContainer).toHaveCSS('z-index', '1000')
await expect(minimap.mainContainer).toHaveCSS('position', 'absolute')
await expect(minimap.mainContainer).toHaveCSS('z-index', '1000')
})

test('Validate minimap toggle button state', async ({ comfyPage }) => {
const minimap = comfyPage.menu.minimap

const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')

await expect(toggleButton).toBeVisible()

const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
await expect(minimapContainer).toBeVisible()
await expect(minimap.container).toBeVisible()
})

test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
const minimap = comfyPage.menu.minimap

// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()

const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')

await expect(minimapContainer).toBeVisible()
await expect(minimap.container).toBeVisible()

await toggleButton.click()
await comfyPage.nextFrame()

await expect(minimapContainer).not.toBeVisible()
await expect(minimap.container).not.toBeVisible()

await toggleButton.click()
await comfyPage.nextFrame()

await expect(minimapContainer).toBeVisible()
await expect(minimap.container).toBeVisible()

// Open zoom controls dropdown again to verify button text
await zoomControlsButton.click()
await comfyPage.nextFrame()

await expect(toggleButton).toContainText('Hide Minimap')
})

test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
const minimap = comfyPage.menu.minimap

await expect(minimapContainer).toBeVisible()
await expect(minimap.container).toBeVisible()

await comfyPage.page.keyboard.press('Alt+KeyM')
await comfyPage.nextFrame()

await expect(minimapContainer).not.toBeVisible()
await expect(minimap.container).not.toBeVisible()

await comfyPage.page.keyboard.press('Alt+KeyM')
await comfyPage.nextFrame()

await expect(minimapContainer).toBeVisible()
await expect(minimap.container).toBeVisible()
})
})
1 change: 1 addition & 0 deletions src/components/graph/GraphCanvasMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
></div>

<ButtonGroup
id="graph-canvas-controls"
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
Expand Down
11 changes: 6 additions & 5 deletions src/composables/useCopy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEventListener } from '@vueuse/core'

import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { isEventTargetInGraph } from '@/workbench/eventHelpers'

/**
* Adds a handler on copy that serializes selected nodes to JSON
Expand All @@ -20,14 +21,14 @@ export const useCopy = () => {
// Default system copy
return
}
const isTargetInGraph =
e.target.classList.contains('litegraph') ||
e.target.classList.contains('graph-canvas-container') ||
e.target.id === 'graph-canvas'
// Check if target is graph canvas or within graph UI (minimap, controls, etc.)
if (!isEventTargetInGraph(e.target)) {
return
}

// copy nodes and clear clipboard
const canvas = canvasStore.canvas
if (isTargetInGraph && canvas?.selectedItems) {
if (canvas?.selectedItems) {
canvas.copyToClipboard()
// clearData doesn't remove images from clipboard
e.clipboardData?.setData('text', ' ')
Expand Down
13 changes: 5 additions & 8 deletions src/composables/usePaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil'
import { isEventTargetInGraph } from '@/workbench/eventHelpers'

/**
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
Expand Down Expand Up @@ -38,14 +39,10 @@ export const usePaste = () => {
}

useEventListener(document, 'paste', async (e) => {
const isTargetInGraph =
e.target instanceof Element &&
(e.target.classList.contains('litegraph') ||
e.target.classList.contains('graph-canvas-container') ||
e.target.id === 'graph-canvas')

// If the target is not in the graph, we don't want to handle the paste event
if (!isTargetInGraph) return
// Check if target is graph canvas or within graph UI (minimap, controls, etc.)
if (!isEventTargetInGraph(e.target)) {
return
}

// ctrl+shift+v is used to paste nodes with connections
// this is handled by litegraph
Expand Down
1 change: 1 addition & 0 deletions src/renderer/extensions/minimap/MiniMap.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div
v-if="visible && initialized"
id="comfy-minimap"
ref="minimapRef"
class="minimap-main-container absolute right-0 bottom-[58px] z-1000 flex"
>
Expand Down
8 changes: 4 additions & 4 deletions src/services/keybindingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ export const useKeybindingService = () => {
const settingStore = useSettingStore()
const dialogStore = useDialogStore()

// Keys that LiteGraph handles but aren't in core keybindings
const canvasBindedKeys = ['Delete', 'Backspace']

// Helper function to determine if an event should be forwarded to canvas
const shouldForwardToCanvas = (event: KeyboardEvent): boolean => {
// Don't forward if modifier keys are pressed (except shift)
if (event.ctrlKey || event.altKey || event.metaKey) {
return false
}

// Keys that LiteGraph handles but aren't in core keybindings
const canvasKeys = ['Delete', 'Backspace']

return canvasKeys.includes(event.key)
return canvasBindedKeys.includes(event.key)
}

const keybindHandler = async function (event: KeyboardEvent) {
Expand Down
31 changes: 31 additions & 0 deletions src/workbench/eventHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Utility functions for handling workbench events
*/

/**
* Checks if an event target is within the graph canvas or related UI elements
* (minimap, canvas controls, etc.)
*
* Used by clipboard handlers to determine if copy/paste events should be
* intercepted for graph operations vs. allowing default browser behavior
* for text inputs and other UI elements.
*
* @param target - The event target to check
* @returns true if the target is within graph-related UI elements
*/
export function isEventTargetInGraph(target: EventTarget | null): boolean {
if (!(target instanceof Element)) {
return false
}

return (
target.id === 'graph-canvas' ||
target.id === 'comfy-minimap' ||
target.id === 'graph-canvas-controls' ||
target.classList.contains('graph-canvas-container') ||
target.classList.contains('litegraph') ||
target.closest('#comfy-minimap') !== null ||
target.closest('#graph-canvas-controls') !== null ||
target.closest('#graph-canvas-container') !== null
)
}
Loading