Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
49 changes: 49 additions & 0 deletions browser_tests/tests/subgraph/subgraphPromotion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,55 @@ test.describe(
'Promoted Widget Visibility in Vue Mode',
{ tag: ['@vue-nodes'] },
() => {
test(
Comment thread
jaeone94 marked this conversation as resolved.
'Promoted advanced widget remains visible when global advanced widgets are disabled',
{ tag: ['@node'] },
async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.Node.AlwaysShowAdvancedWidgets',
false
)
const modelSamplingNode = await comfyPage.nodeOps.addNode(
'ModelSamplingFlux',
{},
{ x: 500, y: 200 }
)
await comfyPage.nextFrame()
await expect(
comfyPage.vueNodes.getNodeLocator(String(modelSamplingNode.id))
).toBeVisible()

await modelSamplingNode.click('title')
const subgraphNode = await modelSamplingNode.convertToSubgraph()
const subgraphNodeId = String(subgraphNode.id)

await comfyPage.vueNodes.enterSubgraph(subgraphNodeId)
const interiorNode =
comfyPage.vueNodes.getNodeByTitle('ModelSamplingFlux')
await expect(interiorNode).toBeVisible()
await interiorNode
.getByText('Show advanced inputs', { exact: true })
.click()
await expect(
interiorNode.getByLabel('max_shift', { exact: true })
).toBeVisible()
await comfyPage.subgraph.promoteWidget(interiorNode, 'max_shift')
await comfyPage.subgraph.exitViaBreadcrumb()

await expectPromotedWidgetNamesToContain(
comfyPage,
subgraphNodeId,
'max_shift'
)

await expect(
comfyPage.vueNodes
.getNodeLocator(subgraphNodeId)
.getByLabel('max_shift', { exact: true })
).toBeVisible()
}
)

test('Promoted text widget renders and enters the subgraph in Vue mode', async ({
comfyPage
}) => {
Expand Down
154 changes: 154 additions & 0 deletions src/components/builder/AppModeWidgetList.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { createTestingPinia } from '@pinia/testing'
import { render, screen } from '@testing-library/vue'
import { fromAny } from '@total-typescript/shoehorn'
import { setActivePinia } from 'pinia'
import { createI18n } from 'vue-i18n'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { toNodeId } from '@/types/nodeId'
import type { WidgetId } from '@/types/widgetId'

import AppModeWidgetList from './AppModeWidgetList.vue'

const mocks = vi.hoisted(() => ({
extractVueNodeData: vi.fn(),
resolvedInputs: { value: [] as unknown[] }
}))

vi.mock('@/components/builder/useResolvedSelectedInputs', () => ({
useResolvedSelectedInputs: () => mocks.resolvedInputs
}))

vi.mock('@/composables/graph/useGraphNodeManager', () => ({
extractVueNodeData: mocks.extractVueNodeData
}))

vi.mock('@/composables/maskeditor/useMaskEditor', () => ({
useMaskEditor: () => ({ openMaskEditor: vi.fn() })
}))

vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({
canvas: { graph: { rootGraph: { id: 'graph-test' } } }
})
}))

vi.mock(
'@/renderer/extensions/vueNodes/composables/useNodeEventHandlers',
() => ({
useNodeEventHandlers: () => ({ handleNodeRightClick: vi.fn() })
})
)

vi.mock('@/renderer/extensions/vueNodes/composables/useNodeTooltips', () => ({
useNodeTooltips: () => ({
createTooltipConfig: () => ({}),
getWidgetTooltip: () => ''
})
}))

vi.mock(
'@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry',
() => ({
getComponent: () => ({
props: ['widget'],
template: '<div data-testid="advanced-widget-control" />'
}),
shouldExpand: () => false,
shouldRenderAsVue: () => true
})
)

vi.mock('@/scripts/app', () => ({
app: {
isGraphReady: false,
rootGraph: { id: 'graph-test' }
}
}))

const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: { remove: 'Remove', rename: 'Rename' }
}
}
})

describe('AppModeWidgetList', () => {
beforeEach(() => {
const widgetId = 'graph-test:1:max_shift' as WidgetId
const widget = fromAny<IBaseWidget, unknown>({
label: 'Max shift',
name: 'max_shift',
widgetId
})
const node = fromAny<LGraphNode, unknown>({
id: toNodeId(1),
mode: LGraphEventMode.ALWAYS,
title: 'Subgraph',
type: 'SubgraphNode'
})
const nodeData: VueNodeData = {
executing: false,
id: toNodeId(1),
inputs: [],
mode: LGraphEventMode.ALWAYS,
outputs: [],
selected: false,
title: 'Subgraph',
type: 'SubgraphNode',
widgets: [
{
name: 'max_shift',
options: { advanced: true },
slotMetadata: {
index: 0,
linked: false,
promoted: true,
type: 'FLOAT'
},
type: 'number',
widgetId
}
]
}

mocks.resolvedInputs.value = [
{
displayName: 'max_shift',
node,
status: 'resolved',
widget,
widgetId
}
]
mocks.extractVueNodeData.mockReturnValue(nodeData)
})

it('renders a selected promoted advanced widget', () => {
const pinia = createTestingPinia({ stubActions: false })
setActivePinia(pinia)

render(AppModeWidgetList, {
global: {
directives: { tooltip: { mounted: () => {} } },
plugins: [pinia, i18n],
stubs: {
Button: { template: '<button><slot /></button>' },
DropZone: { template: '<div><slot /></div>' },
InputSlot: true,
Popover: { template: '<div><slot name="button" /></div>' },
WidgetDescription: true
}
}
})

expect(screen.getByTestId('advanced-widget-control')).toBeVisible()
})
})
10 changes: 7 additions & 3 deletions src/components/builder/AppModeWidgetList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@ const mappedSelections = computed((): WidgetEntry[] => {
})
if (!matchingWidget) return []

matchingWidget.slotMetadata = undefined
matchingWidget.nodeId = node.id
const projectedWidget = {
...matchingWidget,
slotMetadata: undefined,
nodeId: node.id
}

return [
{
Expand All @@ -87,7 +90,8 @@ const mappedSelections = computed((): WidgetEntry[] => {
description: config?.description,
nodeData: {
...fullNodeData,
widgets: [matchingWidget]
showAdvanced: true,
Comment thread
jaeone94 marked this conversation as resolved.
widgets: [projectedWidget]
},
action: { widget, node }
}
Expand Down
4 changes: 3 additions & 1 deletion src/composables/graph/useGraphNodeManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
return { graph, node, upstream, linkId: link.id }
}

it('sets slotMetadata.linked to true when input has a link', () => {
it('identifies a linked regular widget input as unpromoted', () => {
const { graph, node } = createWidgetInputGraph()
const { vueNodeData } = useGraphNodeManager(graph)

Expand All @@ -121,6 +121,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {

expect(widgetData?.slotMetadata).toBeDefined()
expect(widgetData?.slotMetadata?.linked).toBe(true)
expect(widgetData?.slotMetadata?.promoted).toBe(false)
})

it('updates slotMetadata.linked to false after link disconnect event', async () => {
Expand Down Expand Up @@ -236,6 +237,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
expect(widgetData).toBeDefined()
expect(widgetData?.sourceWidgetName).toBe('prompt')
expect(widgetData?.slotMetadata).toBeDefined()
expect(widgetData?.slotMetadata?.promoted).toBe(true)
})

it('clears stale slotMetadata when input no longer matches widget', async () => {
Expand Down
2 changes: 2 additions & 0 deletions src/composables/graph/useGraphNodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface WidgetSlotMetadata {
linked: boolean
originNodeId?: NodeId
originOutputName?: string
promoted: boolean
type: string
}

Expand Down Expand Up @@ -352,6 +353,7 @@ function buildSlotMetadata(
linked: input.link != null,
originNodeId,
originOutputName,
promoted: input.widgetId !== undefined,
Comment thread
jaeone94 marked this conversation as resolved.
Comment thread
jaeone94 marked this conversation as resolved.
type: String(input.type)
}
if (input.name) metadata.set(input.name, slotInfo)
Expand Down
1 change: 1 addition & 0 deletions src/lib/litegraph/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ export interface IWidgetLocator {
export interface INodeInputSlot extends INodeSlot {
link: LinkId | null
widget?: IWidgetLocator
/** Host-owned promoted widget identity for a subgraph input (ADR 0009). */
widgetId?: WidgetId
alwaysVisible?: boolean

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ describe('isWidgetVisible', () => {
expect(isWidgetVisible({ advanced: true }, true)).toBe(true)
})

it('keeps advanced widgets visible when linked and showAdvanced is false', () => {
it('keeps advanced widgets visible when advanced filtering is ignored', () => {
expect(isWidgetVisible({ advanced: true }, false, true)).toBe(true)
})

it('keeps hidden widgets hidden when linked', () => {
it('keeps hidden widgets hidden when advanced filtering is ignored', () => {
expect(isWidgetVisible({ hidden: true }, false, true)).toBe(false)
})
})
Expand Down Expand Up @@ -408,6 +408,34 @@ describe('computeProcessedWidgets missing media', () => {
})
})

describe('computeProcessedWidgets visibility', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})

it('keeps a promoted advanced widget visible without source metadata', () => {
const widget = createMockWidget({
name: 'max_shift',
type: 'number',
options: { advanced: true },
slotMetadata: {
index: 0,
linked: false,
promoted: true,
Comment thread
jaeone94 marked this conversation as resolved.
type: 'FLOAT'
},
sourceExecutionId: undefined,
sourceWidgetName: undefined
})

const promotedWidget = processWidgets([widget])[0]

expect(promotedWidget).toBeDefined()
expect(promotedWidget?.visible).toBe(true)
expect(promotedWidget?.simplified.borderStyle).toBeDefined()
})
})

describe('computeProcessedWidgets borderStyle', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,11 @@ function getWidgetNodeLocatorId(
export function isWidgetVisible(
options: IWidgetOptions,
showAdvanced: boolean,
linked = false
ignoreAdvanced = false
): boolean {
const hidden = options.hidden ?? false
const advanced = options.advanced ?? false
return !hidden && (!advanced || showAdvanced || linked)
return !hidden && (!advanced || showAdvanced || ignoreAdvanced)
}

export function computeProcessedWidgets({
Expand Down Expand Up @@ -275,7 +275,7 @@ export function computeProcessedWidgets({
const visible = isWidgetVisible(
mergedOptions,
showAdvanced,
widget.slotMetadata?.linked
widget.slotMetadata?.linked || widget.slotMetadata?.promoted
)
if (!identity.dedupeIdentity) {
uniqueWidgets.push({
Expand Down
Loading