Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ export const comfyPageFixture = base.extend<{
// Disable errors tab to prevent missing model detection from
// rendering error indicators on nodes during unrelated tests.
'Comfy.RightSidePanel.ShowErrorsTab': false,
'Comfy.VueNodes.CompactCollapsedNodes': false,
'Comfy.VueNodes.HideStatusBadges': false,
...(isVueNodes && { 'Comfy.VueNodes.Enabled': true }),
...initialSettings
})
Expand Down
175 changes: 175 additions & 0 deletions browser_tests/tests/creditHelpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from '@playwright/test'

import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'

function buildMockApiNode(
Expand Down Expand Up @@ -211,5 +212,179 @@ testWithMockedObjectInfo.describe(
.toContain('4.2/10.6')
}
)

for (const { compact, hideStatus } of [
{ compact: false, hideStatus: false },
{ compact: false, hideStatus: true },
{ compact: true, hideStatus: false },
{ compact: true, hideStatus: true }
] as const) {
testWithMockedObjectInfo(
`keeps collapsed badges independent with compact=${compact} and hideStatus=${hideStatus}`,
async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.ShowApiPricing',
true
)
await comfyPage.settings.setSetting(
'Comfy.VueNodes.CompactCollapsedNodes',
compact
)
await comfyPage.settings.setSetting(
'Comfy.VueNodes.HideStatusBadges',
hideStatus
)
await comfyPage.nodeOps.clearGraph()

const nodeId = await comfyPage.page.evaluate((mode) => {
const node = window.LiteGraph!.createNode('TestCreditApiNodeUsd')!
node.mode = mode
node.title = 'API'
node.setPos(100, 100)
node.setSize([400, node.size[1]])
window.app!.graph.add(node)
return node.id
}, LGraphEventMode.BYPASS)

await comfyPage.vueNodes.waitForNodes(1)
const vueNode = comfyPage.vueNodes.getNodeLocator(String(nodeId))
const header = vueNode.locator(
`[data-testid="node-header-${nodeId}"]`
)
const collapseButton = vueNode.getByTestId('node-collapse-button')
const pricing = header.locator(
'span:has(> i[class*="lucide--component"])'
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const status = header.getByText('Bypassed', { exact: true })

await expect(pricing).toContainText('10.6')
await expect(status).toHaveCount(hideStatus ? 0 : 1)
const expandedBox = await vueNode.boundingBox()
if (!expandedBox) throw new Error('Expanded node has no bounds')

await collapseButton.press('Enter')
await expect(vueNode).toHaveAttribute('data-collapsed', 'true')
await expect(pricing).toHaveCount(compact ? 0 : 1)
await expect(status).toHaveCount(compact || hideStatus ? 0 : 1)

if (compact) {
await expect
.poll(async () => (await vueNode.boundingBox())?.width)
.toBeLessThan(expandedBox.width)
} else {
await expect
.poll(() =>
vueNode.evaluate((element) =>
Number.parseFloat(getComputedStyle(element).width)
)
)
.toBeGreaterThanOrEqual(225)
}

await collapseButton.press('Enter')
await expect(pricing).toHaveCount(1)
await expect(status).toHaveCount(hideStatus ? 0 : 1)
await expect
.poll(async () => (await vueNode.boundingBox())?.width)
.toBeCloseTo(expandedBox.width, 0)
}
)
}

testWithMockedObjectInfo(
'updates compact and status settings while a node is mounted',
async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.ShowApiPricing',
true
)
await comfyPage.settings.setSetting(
'Comfy.VueNodes.CompactCollapsedNodes',
false
)
await comfyPage.settings.setSetting(
'Comfy.VueNodes.HideStatusBadges',
false
)
await comfyPage.nodeOps.clearGraph()

const nodeId = await comfyPage.page.evaluate((mode) => {
const node = window.LiteGraph!.createNode('TestCreditApiNodeUsd')!
node.mode = mode
node.title = 'API'
node.setPos(100, 100)
node.setSize([400, node.size[1]])
window.app!.graph.add(node)
return node.id
}, LGraphEventMode.NEVER)

await comfyPage.vueNodes.waitForNodes(1)
const nodeFixture = await comfyPage.vueNodes.getFixtureByTitle('API')
const vueNode = comfyPage.vueNodes.getNodeLocator(String(nodeId))
const header = vueNode.locator(`[data-testid="node-header-${nodeId}"]`)
const pricing = header.locator(
'span:has(> i[class*="lucide--component"])'
)
const status = header.getByText('Muted', { exact: true })

await expect(pricing).toContainText('10.6')
await nodeFixture.collapseButton.press('Enter')
await expect(pricing).toHaveCount(1)
await expect(status).toHaveCount(1)
const normalCollapsedBox = await vueNode.boundingBox()
if (!normalCollapsedBox) throw new Error('Collapsed node has no bounds')

await comfyPage.settings.setSetting(
'Comfy.VueNodes.CompactCollapsedNodes',
true
)
await expect(pricing).toHaveCount(0)
await expect(status).toHaveCount(0)
await expect
.poll(async () => (await vueNode.boundingBox())?.width)
.toBeLessThan(normalCollapsedBox.width)

await comfyPage.settings.setSetting(
'Comfy.VueNodes.CompactCollapsedNodes',
false
)
await expect(pricing).toHaveCount(1)
await expect(status).toHaveCount(1)

await comfyPage.settings.setSetting(
'Comfy.VueNodes.HideStatusBadges',
true
)
await expect(status).toHaveCount(0)
await expect(pricing).toHaveCount(1)

await nodeFixture.collapseButton.press('Enter')
await expect(status).toHaveCount(0)
await expect(pricing).toHaveCount(1)

await comfyPage.settings.setSetting(
'Comfy.VueNodes.HideStatusBadges',
false
)
await expect(status).toHaveCount(1)

await comfyPage.settings.setSetting(
'Comfy.VueNodes.CompactCollapsedNodes',
true
)
await nodeFixture.setTitle(
'A deliberately long API node title that must remain width capped'
)
const expandedBox = await vueNode.boundingBox()
if (!expandedBox) throw new Error('Expanded node has no bounds')

await nodeFixture.collapseButton.press('Enter')
await expect
.poll(async () => (await vueNode.boundingBox())?.width)
.toBeLessThanOrEqual(expandedBox.width)
}
)
}
)
8 changes: 8 additions & 0 deletions src/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -426,10 +426,18 @@
"Comfy_Validation_Workflows": {
"name": "Validate workflows"
},
"Comfy_VueNodes_CompactCollapsedNodes": {
"name": "Compact collapsed nodes",
"tooltip": "Size collapsed nodes to their titles and hide header badges while collapsed."
},
"Comfy_VueNodes_Enabled": {
"name": "Modern Node Design (Nodes 2.0)",
"tooltip": "Modern: DOM-based rendering with enhanced interactivity, native browser features, and updated visual design. Classic: Traditional canvas rendering."
},
"Comfy_VueNodes_HideStatusBadges": {
"name": "Hide node status badges",
"tooltip": "Hide Muted and Bypassed badges on Nodes 2.0."
},
"Comfy_WidgetControlMode": {
"name": "Widget control mode",
"tooltip": "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
Expand Down
23 changes: 23 additions & 0 deletions src/platform/settings/constants/coreSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,29 @@ export const CORE_SETTINGS: SettingParams[] = [
experimental: true,
versionAdded: '1.27.1'
},
{
id: 'Comfy.VueNodes.CompactCollapsedNodes',
category: ['Comfy', 'Nodes 2.0', 'CompactCollapsedNodes'],
name: 'Compact collapsed nodes',
type: 'boolean',
tooltip:
'Size collapsed nodes to their titles and hide header badges while collapsed.',
defaultValue: false,
sortOrder: 85,
experimental: true,
versionAdded: '1.49.0'
},
{
id: 'Comfy.VueNodes.HideStatusBadges',
category: ['Comfy', 'Nodes 2.0', 'HideStatusBadges'],
name: 'Hide node status badges',
type: 'boolean',
tooltip: 'Hide Muted and Bypassed badges on Nodes 2.0.',
defaultValue: false,
sortOrder: 80,
experimental: true,
versionAdded: '1.49.0'
},
{
id: 'Comfy.AppBuilder.VueNodeSwitchDismissed',
name: 'App Builder Vue Node switch dismissed',
Expand Down
27 changes: 24 additions & 3 deletions src/renderer/extensions/vueNodes/components/LGraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
: 'drop-shadow-xl drop-shadow-black/40',
isRerouteNode
? 'h-(--node-height)'
: 'min-h-(--node-height) min-w-(--min-node-width)',
: cn(
'min-h-(--node-height)',
!compactCollapsed && 'min-w-(--min-node-width)'
),
cursorClass,
isSelected && 'outline-node-component-outline',
executing && 'outline-node-stroke-executing',
Expand Down Expand Up @@ -73,7 +76,10 @@
cn(
'flex flex-1 flex-col bg-node-component-header-surface',
'w-(--node-width)',
!isRerouteNode && 'min-w-(--min-node-width)',
!isRerouteNode &&
(compactCollapsed
? 'max-w-(--node-width-x)'
: 'min-w-(--min-node-width)'),
shapeClass,
hasAnyError && 'ring-4 ring-destructive-background',
bypassed && bypassOverlayClass,
Expand Down Expand Up @@ -106,7 +112,9 @@
<NodeHeader
:node-data="nodeData"
:collapsed="isCollapsed"
:price-badges="badges.pricing"
:compact="compactCollapsed"
:price-badges="headerPricingBadges"
:show-status-badge="showStatusBadge"
@collapse="handleCollapse"
@update:title="handleHeaderTitleUpdate"
/>
Expand Down Expand Up @@ -391,6 +399,16 @@ const displayHeader = computed(() => nodeData.titleMode !== TitleMode.NO_TITLE)
const isRerouteNode = computed(() => nodeData.type === 'Reroute')

const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)
const compactCollapsed = computed(
() =>
isCollapsed.value &&
settingStore.get('Comfy.VueNodes.CompactCollapsedNodes')
)
const showStatusBadge = computed(
() =>
!compactCollapsed.value &&
!settingStore.get('Comfy.VueNodes.HideStatusBadges')
)
const bypassed = computed(
(): boolean => nodeData.mode === LGraphEventMode.BYPASS
)
Expand Down Expand Up @@ -430,6 +448,9 @@ const { pointerHandlers } = useNodePointerInteractions(() => nodeData.id)
const { onPointerdown, ...remainingPointerHandlers } = pointerHandlers
const { startDrag } = useNodeDrag()
const badges = usePartitionedBadges(nodeData)
const headerPricingBadges = computed(() =>
compactCollapsed.value ? undefined : badges.value.pricing
)

async function nodeOnPointerdown(event: PointerEvent) {
if (event.altKey && lgraphNode.value) {
Expand Down
40 changes: 40 additions & 0 deletions src/renderer/extensions/vueNodes/components/NodeHeader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { ComponentProps } from 'vue-component-type-helpers'
import { createI18n } from 'vue-i18n'

import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
import enMessages from '@/locales/en/main.json'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { Settings } from '@/schemas/apiSchema'
Expand Down Expand Up @@ -205,6 +206,45 @@ describe('NodeHeader.vue', () => {
expect(collapsedIcon.classList).toContain('-rotate-90')
})

it.for([
{ mode: LGraphEventMode.NEVER, label: 'Muted' },
{ mode: LGraphEventMode.BYPASS, label: 'Bypassed' }
])(
'controls the $label status badge independently',
async ({ mode, label }) => {
const { rerender } = renderHeader({
nodeData: makeNodeData({ mode }),
priceBadges: [{ required: '10.6', rest: 'credits' }]
})

expect(screen.getByText(label)).toBeInTheDocument()
expect(screen.getByText('10.6')).toBeInTheDocument()

await rerender({
nodeData: makeNodeData({ mode }),
collapsed: false,
priceBadges: [{ required: '10.6', rest: 'credits' }],
showStatusBadge: false
})

expect(screen.queryByText(label)).not.toBeInTheDocument()
expect(screen.getByText('10.6')).toBeInTheDocument()
}
)

it('keeps the pin indicator when status badges are hidden', () => {
renderHeader({
nodeData: makeNodeData({
mode: LGraphEventMode.BYPASS,
flags: { pinned: true }
}),
showStatusBadge: false
})

expect(screen.queryByText('Bypassed')).not.toBeInTheDocument()
expect(screen.getByTestId('node-pin-indicator')).toBeInTheDocument()
})

describe('Tooltips', () => {
it('applies tooltip directive to node title with correct configuration', () => {
const { tooltipDirective } = renderHeader({
Expand Down
Loading
Loading