mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-08 08:08:04 +00:00
Compare commits
4 Commits
fix/websit
...
glary/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd28179a17 | ||
|
|
0fc033de13 | ||
|
|
b3a151710b | ||
|
|
f9fd347a15 |
155
browser_tests/fixtures/data/nodeDefinitions.ts
Normal file
155
browser_tests/fixtures/data/nodeDefinitions.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
|
||||
/**
|
||||
* Base node definitions covering the default workflow.
|
||||
* Use {@link createMockNodeDefinitions} to extend with per-test overrides.
|
||||
*/
|
||||
const baseNodeDefinitions: Record<string, ComfyNodeDef> = {
|
||||
KSampler: {
|
||||
input: {
|
||||
required: {
|
||||
model: ['MODEL', {}],
|
||||
seed: [
|
||||
'INT',
|
||||
{
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 0xfffffffffffff,
|
||||
control_after_generate: true
|
||||
}
|
||||
],
|
||||
steps: ['INT', { default: 20, min: 1, max: 10000 }],
|
||||
cfg: ['FLOAT', { default: 8.0, min: 0.0, max: 100.0, step: 0.1 }],
|
||||
sampler_name: [['euler', 'euler_ancestral', 'heun', 'dpm_2'], {}],
|
||||
scheduler: [['normal', 'karras', 'exponential', 'simple'], {}],
|
||||
positive: ['CONDITIONING', {}],
|
||||
negative: ['CONDITIONING', {}],
|
||||
latent_image: ['LATENT', {}]
|
||||
},
|
||||
optional: {
|
||||
denoise: ['FLOAT', { default: 1.0, min: 0.0, max: 1.0, step: 0.01 }]
|
||||
}
|
||||
},
|
||||
output: ['LATENT'],
|
||||
output_is_list: [false],
|
||||
output_name: ['LATENT'],
|
||||
name: 'KSampler',
|
||||
display_name: 'KSampler',
|
||||
description: 'Samples latents using the provided model and conditioning.',
|
||||
category: 'sampling',
|
||||
output_node: false,
|
||||
python_module: 'nodes',
|
||||
deprecated: false,
|
||||
experimental: false
|
||||
},
|
||||
|
||||
CheckpointLoaderSimple: {
|
||||
input: {
|
||||
required: {
|
||||
ckpt_name: [
|
||||
['v1-5-pruned.safetensors', 'sd_xl_base_1.0.safetensors'],
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
output: ['MODEL', 'CLIP', 'VAE'],
|
||||
output_is_list: [false, false, false],
|
||||
output_name: ['MODEL', 'CLIP', 'VAE'],
|
||||
name: 'CheckpointLoaderSimple',
|
||||
display_name: 'Load Checkpoint',
|
||||
description: 'Loads a diffusion model checkpoint.',
|
||||
category: 'loaders',
|
||||
output_node: false,
|
||||
python_module: 'nodes',
|
||||
deprecated: false,
|
||||
experimental: false
|
||||
},
|
||||
|
||||
CLIPTextEncode: {
|
||||
input: {
|
||||
required: {
|
||||
text: ['STRING', { multiline: true, dynamicPrompts: true }],
|
||||
clip: ['CLIP', {}]
|
||||
}
|
||||
},
|
||||
output: ['CONDITIONING'],
|
||||
output_is_list: [false],
|
||||
output_name: ['CONDITIONING'],
|
||||
name: 'CLIPTextEncode',
|
||||
display_name: 'CLIP Text Encode (Prompt)',
|
||||
description: 'Encodes a text prompt using a CLIP model.',
|
||||
category: 'conditioning',
|
||||
output_node: false,
|
||||
python_module: 'nodes',
|
||||
deprecated: false,
|
||||
experimental: false
|
||||
},
|
||||
|
||||
EmptyLatentImage: {
|
||||
input: {
|
||||
required: {
|
||||
width: ['INT', { default: 512, min: 16, max: 16384, step: 8 }],
|
||||
height: ['INT', { default: 512, min: 16, max: 16384, step: 8 }],
|
||||
batch_size: ['INT', { default: 1, min: 1, max: 4096 }]
|
||||
}
|
||||
},
|
||||
output: ['LATENT'],
|
||||
output_is_list: [false],
|
||||
output_name: ['LATENT'],
|
||||
name: 'EmptyLatentImage',
|
||||
display_name: 'Empty Latent Image',
|
||||
description: 'Creates an empty latent image of the specified dimensions.',
|
||||
category: 'latent',
|
||||
output_node: false,
|
||||
python_module: 'nodes',
|
||||
deprecated: false,
|
||||
experimental: false
|
||||
},
|
||||
|
||||
VAEDecode: {
|
||||
input: {
|
||||
required: {
|
||||
samples: ['LATENT', {}],
|
||||
vae: ['VAE', {}]
|
||||
}
|
||||
},
|
||||
output: ['IMAGE'],
|
||||
output_is_list: [false],
|
||||
output_name: ['IMAGE'],
|
||||
name: 'VAEDecode',
|
||||
display_name: 'VAE Decode',
|
||||
description: 'Decodes latent images back into pixel space.',
|
||||
category: 'latent',
|
||||
output_node: false,
|
||||
python_module: 'nodes',
|
||||
deprecated: false,
|
||||
experimental: false
|
||||
},
|
||||
|
||||
SaveImage: {
|
||||
input: {
|
||||
required: {
|
||||
images: ['IMAGE', {}],
|
||||
filename_prefix: ['STRING', { default: 'ComfyUI' }]
|
||||
}
|
||||
},
|
||||
output: [],
|
||||
output_is_list: [],
|
||||
output_name: [],
|
||||
name: 'SaveImage',
|
||||
display_name: 'Save Image',
|
||||
description: 'Saves images to the output directory.',
|
||||
category: 'image',
|
||||
output_node: true,
|
||||
python_module: 'nodes',
|
||||
deprecated: false,
|
||||
experimental: false
|
||||
}
|
||||
}
|
||||
|
||||
export function createMockNodeDefinitions(
|
||||
overrides?: Record<string, ComfyNodeDef>
|
||||
): Record<string, ComfyNodeDef> {
|
||||
const base = structuredClone(baseNodeDefinitions)
|
||||
return overrides ? { ...base, ...overrides } : base
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
92
browser_tests/tests/nodeSearchBoxV2Essentials.spec.ts
Normal file
92
browser_tests/tests/nodeSearchBoxV2Essentials.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { createMockNodeDefinitions } from '@e2e/fixtures/data/nodeDefinitions'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
const CORE_ESSENTIAL = 'FE568CoreEssential'
|
||||
const CUSTOM_PACK_ESSENTIAL = 'FE568CustomPackEssential'
|
||||
|
||||
const fixtureDefs = createMockNodeDefinitions({
|
||||
[CORE_ESSENTIAL]: {
|
||||
input: { required: {}, optional: {} },
|
||||
output: ['IMAGE'],
|
||||
output_name: ['image'],
|
||||
output_is_list: [false],
|
||||
output_node: false,
|
||||
name: CORE_ESSENTIAL,
|
||||
display_name: 'FE568 Core Essential',
|
||||
description: 'Core essential — FE-568 regression fixture',
|
||||
category: 'image/upscaling',
|
||||
python_module: 'comfy_extras.nodes_images',
|
||||
essentials_category: 'image tools'
|
||||
},
|
||||
[CUSTOM_PACK_ESSENTIAL]: {
|
||||
input: { required: {}, optional: {} },
|
||||
output: ['IMAGE'],
|
||||
output_name: ['image'],
|
||||
output_is_list: [false],
|
||||
output_node: false,
|
||||
name: CUSTOM_PACK_ESSENTIAL,
|
||||
display_name: 'FE568 Custom Pack Essential',
|
||||
description: 'Custom-pack essential — FE-568 regression fixture',
|
||||
category: 'KJNodes/masking',
|
||||
python_module: 'custom_nodes.comfyui-kjnodes',
|
||||
essentials_category: 'image tools'
|
||||
}
|
||||
})
|
||||
|
||||
test.describe(
|
||||
'Node search box V2 — Essentials/Extensions classification (FE-568)',
|
||||
{ tag: '@node' },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.page.route('**/api/object_info', (route) =>
|
||||
route.fulfill({ json: fixtureDefs })
|
||||
)
|
||||
await comfyPage.workflow.reloadAndWaitForApp()
|
||||
// The Essentials chip is gated behind this server feature flag. In a
|
||||
// production build (as run in CI) it defaults to off, so enable it
|
||||
// explicitly to render the chip deterministically.
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window.app!.api.serverFeatureFlags.value = {
|
||||
...window.app!.api.serverFeatureFlags.value,
|
||||
node_library_essentials_enabled: true
|
||||
}
|
||||
})
|
||||
await comfyPage.searchBoxV2.setup()
|
||||
})
|
||||
|
||||
test('Essentials chip excludes custom-pack nodes that declare essentials_category', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
await searchBoxV2.open()
|
||||
await searchBoxV2.rootCategoryButton('essentials').click()
|
||||
await searchBoxV2.input.fill('FE568')
|
||||
|
||||
await expect(
|
||||
searchBoxV2.results.filter({ hasText: 'FE568 Core Essential' })
|
||||
).toHaveCount(1)
|
||||
await expect(
|
||||
searchBoxV2.results.filter({ hasText: 'FE568 Custom Pack Essential' })
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Extensions chip includes custom-pack nodes that declare essentials_category', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { searchBoxV2 } = comfyPage
|
||||
await searchBoxV2.open()
|
||||
await searchBoxV2.rootCategoryButton('custom').click()
|
||||
await searchBoxV2.input.fill('FE568')
|
||||
|
||||
await expect(
|
||||
searchBoxV2.results.filter({ hasText: 'FE568 Custom Pack Essential' })
|
||||
).toHaveCount(1)
|
||||
await expect(
|
||||
searchBoxV2.results.filter({ hasText: 'FE568 Core Essential' })
|
||||
).toHaveCount(0)
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -3,8 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
NodeSourceType,
|
||||
getNodeSource,
|
||||
isCustomNode,
|
||||
isEssentialNode
|
||||
isCustomNode
|
||||
} from '../classifiers/nodeSource'
|
||||
import type { NodeSource } from '../classifiers/nodeSource'
|
||||
|
||||
@@ -79,27 +78,14 @@ describe('getNodeSource', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('essentials nodes', () => {
|
||||
it('should identify essentials nodes when essentials_category is set', () => {
|
||||
const result = getNodeSource('nodes.some_module', 'Image')
|
||||
expect(result.type).toBe(NodeSourceType.Essentials)
|
||||
expect(result.className).toBe('comfy-essentials')
|
||||
})
|
||||
it('should return UNKNOWN_NODE_SOURCE for custom_nodes with no pack segment', () => {
|
||||
expect(getNodeSource('custom_nodes').type).toBe(NodeSourceType.Unknown)
|
||||
})
|
||||
|
||||
it('should identify essentials nodes from custom_nodes module', () => {
|
||||
const result = getNodeSource(
|
||||
'custom_nodes.ComfyUI-Example@1.0.0',
|
||||
'Video'
|
||||
)
|
||||
expect(result.type).toBe(NodeSourceType.Essentials)
|
||||
expect(result.className).toBe('comfy-essentials')
|
||||
expect(result.displayText).toBe('Example')
|
||||
})
|
||||
|
||||
it('should not identify nodes without essentials_category as essentials', () => {
|
||||
const result = getNodeSource('nodes.some_module', undefined)
|
||||
expect(result.type).toBe(NodeSourceType.Core)
|
||||
})
|
||||
it('should strip ComfyUI- and -ComfyUI prefixes/suffixes from custom pack names', () => {
|
||||
expect(getNodeSource('custom_nodes.ComfyUI-foo').displayText).toBe('foo')
|
||||
expect(getNodeSource('custom_nodes.bar-ComfyUI').displayText).toBe('bar')
|
||||
expect(getNodeSource('custom_nodes.Comfy_baz').displayText).toBe('baz')
|
||||
})
|
||||
|
||||
describe('blueprint nodes', () => {
|
||||
@@ -126,21 +112,6 @@ function makeNode(type: NodeSourceType): { nodeSource: NodeSource } {
|
||||
}
|
||||
}
|
||||
|
||||
describe('isEssentialNode', () => {
|
||||
it('returns true for Essentials nodes', () => {
|
||||
expect(isEssentialNode(makeNode(NodeSourceType.Essentials))).toBe(true)
|
||||
})
|
||||
|
||||
it.for([
|
||||
NodeSourceType.Core,
|
||||
NodeSourceType.CustomNodes,
|
||||
NodeSourceType.Blueprint,
|
||||
NodeSourceType.Unknown
|
||||
])('returns false for %s nodes', (type) => {
|
||||
expect(isEssentialNode(makeNode(type))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isCustomNode', () => {
|
||||
it('returns true for CustomNodes', () => {
|
||||
expect(isCustomNode(makeNode(NodeSourceType.CustomNodes))).toBe(true)
|
||||
@@ -148,7 +119,6 @@ describe('isCustomNode', () => {
|
||||
|
||||
it.for([
|
||||
NodeSourceType.Core,
|
||||
NodeSourceType.Essentials,
|
||||
NodeSourceType.Unknown,
|
||||
NodeSourceType.Blueprint
|
||||
])('returns false for %s nodes', (type) => {
|
||||
|
||||
@@ -4,7 +4,6 @@ export enum NodeSourceType {
|
||||
Core = 'core',
|
||||
CustomNodes = 'custom_nodes',
|
||||
Blueprint = 'blueprint',
|
||||
Essentials = 'essentials',
|
||||
Unknown = 'unknown'
|
||||
}
|
||||
export const CORE_NODE_MODULES = ['nodes', 'comfy_extras', 'comfy_api_nodes']
|
||||
@@ -29,24 +28,12 @@ function shortenNodeName(name: string) {
|
||||
.replace(/(-ComfyUI|_ComfyUI|-Comfy|_Comfy)$/, '')
|
||||
}
|
||||
|
||||
export function getNodeSource(
|
||||
python_module?: string,
|
||||
essentials_category?: string
|
||||
): NodeSource {
|
||||
export function getNodeSource(python_module?: string): NodeSource {
|
||||
if (!python_module) {
|
||||
return UNKNOWN_NODE_SOURCE
|
||||
}
|
||||
const modules = python_module.split('.')
|
||||
if (essentials_category) {
|
||||
const moduleName = modules[1] ?? modules[0] ?? 'essentials'
|
||||
const displayName = shortenNodeName(moduleName.split('@')[0])
|
||||
return {
|
||||
type: NodeSourceType.Essentials,
|
||||
className: 'comfy-essentials',
|
||||
displayText: displayName,
|
||||
badgeText: displayName
|
||||
}
|
||||
} else if (CORE_NODE_MODULES.includes(modules[0])) {
|
||||
if (CORE_NODE_MODULES.includes(modules[0])) {
|
||||
return {
|
||||
type: NodeSourceType.Core,
|
||||
className: 'comfy-core',
|
||||
@@ -82,10 +69,6 @@ interface NodeDefLike {
|
||||
nodeSource: NodeSource
|
||||
}
|
||||
|
||||
export function isEssentialNode(node: NodeDefLike): boolean {
|
||||
return node.nodeSource.type === NodeSourceType.Essentials
|
||||
}
|
||||
|
||||
export function isCustomNode(node: NodeDefLike): boolean {
|
||||
return node.nodeSource.type === NodeSourceType.CustomNodes
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function groupNodesByPack(
|
||||
const byPackId = new Map<string, NodePack>()
|
||||
|
||||
for (const [className, def] of Object.entries(defs)) {
|
||||
const source = getNodeSource(def.python_module, def.essentials_category)
|
||||
const source = getNodeSource(def.python_module)
|
||||
if (source.type !== NodeSourceType.CustomNodes) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -230,6 +230,60 @@ describe('NodeSearchContent', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should not show custom-pack nodes under Essentials even when essentials_category is set', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
name: 'CoreEssential',
|
||||
display_name: 'Core Essential',
|
||||
essentials_category: 'image tools',
|
||||
python_module: 'comfy_extras.nodes_images'
|
||||
}),
|
||||
createMockNodeDef({
|
||||
name: 'CustomPackEssential',
|
||||
display_name: 'Custom Pack Essential',
|
||||
essentials_category: 'image tools',
|
||||
python_module: 'custom_nodes.comfyui-kjnodes',
|
||||
category: 'KJNodes/masking'
|
||||
})
|
||||
])
|
||||
|
||||
const { user } = renderComponent()
|
||||
await clickFilterBarButton(user, 'Essentials')
|
||||
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Core Essential')
|
||||
})
|
||||
})
|
||||
|
||||
it('should expose custom-pack nodes under Extensions even when essentials_category is set', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
name: 'CustomPackEssential',
|
||||
display_name: 'Custom Pack Essential',
|
||||
essentials_category: 'image tools',
|
||||
python_module: 'custom_nodes.comfyui-kjnodes',
|
||||
category: 'KJNodes/masking'
|
||||
}),
|
||||
createMockNodeDef({
|
||||
name: 'CoreEssential',
|
||||
display_name: 'Core Essential',
|
||||
essentials_category: 'image tools',
|
||||
python_module: 'comfy_extras.nodes_images'
|
||||
})
|
||||
])
|
||||
|
||||
const { user } = renderComponent()
|
||||
await clickFilterBarButton(user, 'Extensions')
|
||||
|
||||
await waitFor(() => {
|
||||
const items = screen.getAllByTestId('node-item')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0]).toHaveTextContent('Custom Pack Essential')
|
||||
})
|
||||
})
|
||||
|
||||
it('should show only API nodes when Partner Nodes filter is active', async () => {
|
||||
useNodeDefStore().updateNodeDefs([
|
||||
createMockNodeDef({
|
||||
|
||||
@@ -126,15 +126,18 @@ import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { resolveEssentialsCategory } from '@/services/nodeOrganizationService'
|
||||
import {
|
||||
BLUEPRINT_CATEGORY,
|
||||
isCustomNode,
|
||||
isEssentialNode,
|
||||
NodeSourceType
|
||||
} from '@/types/nodeSource'
|
||||
import type { FuseFilter, FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const isEssentialNode = (n: ComfyNodeDefImpl) =>
|
||||
resolveEssentialsCategory(n) !== undefined
|
||||
|
||||
const sourceCategoryFilters: Record<string, (n: ComfyNodeDefImpl) => boolean> =
|
||||
{
|
||||
[RootCategory.Essentials]: isEssentialNode,
|
||||
|
||||
@@ -22,7 +22,7 @@ import { sortedTree, unwrapTreeRoot } from '@/utils/treeUtil'
|
||||
const DEFAULT_ICON = 'pi pi-sort'
|
||||
const UNKNOWN_RANK = Number.MAX_SAFE_INTEGER
|
||||
|
||||
function resolveEssentialsCategory(
|
||||
export function resolveEssentialsCategory(
|
||||
nodeDef: ComfyNodeDefImpl
|
||||
): EssentialsCategory | undefined {
|
||||
if (!nodeDef.isCoreNode) return undefined
|
||||
@@ -317,10 +317,7 @@ class NodeOrganizationService {
|
||||
else myBlueprints.push(node)
|
||||
} else if (node.api_node || node.category?.startsWith('api node')) {
|
||||
partnerNodes.push(node)
|
||||
} else if (
|
||||
node.nodeSource.type === NodeSourceType.Core ||
|
||||
node.nodeSource.type === NodeSourceType.Essentials
|
||||
) {
|
||||
} else if (node.nodeSource.type === NodeSourceType.Core) {
|
||||
comfyNodes.push(node)
|
||||
} else {
|
||||
extensions.push(node)
|
||||
|
||||
@@ -179,8 +179,7 @@ export class ComfyNodeDefImpl
|
||||
this.outputs = defV2.outputs
|
||||
this.hidden = defV2.hidden
|
||||
|
||||
// Initialize node source
|
||||
this.nodeSource = getNodeSource(obj.python_module, this.essentials_category)
|
||||
this.nodeSource = getNodeSource(obj.python_module)
|
||||
this.inputTypes = _.uniq(
|
||||
Object.values(this.inputs).flatMap(resolveInputType)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user