Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions
dd28179a17 [automated] Update test expectations 2026-06-19 15:22:03 -07:00
bymyself
0fc033de13 test: enable essentials flag and restore node def fixture for FE-568 e2e
The Essentials chip is gated behind node_library_essentials_enabled, which
defaults off in production builds (CI). Enable it explicitly so the chip
renders deterministically. Restore the nodeDefinitions fixture deleted by
#12496 as unused (this spec is now its consumer).
2026-06-19 14:50:48 -07:00
Glary-Bot
b3a151710b test: bump nodeSearchBoxV2Essentials timeout for CI
The beforeEach hook re-routes /api/object_info with fixture defs and
calls reloadAndWaitForApp. On a CI runner the route + reload + waitForApp
exceed the default 15s test timeout; locally it runs in ~21s. Bump the
spec timeout to 60s, matching the pattern in versionMismatchWarnings,
keybindingPresets, and similar specs that need a fresh app boot in
beforeEach.
2026-06-19 14:40:18 -07:00
Glary-Bot
f9fd347a15 FE-568: stop letting essentials_category overwrite nodeSource.type
getNodeSource set type=Essentials whenever essentials_category was
populated, which erased the python_module classification — so a node
shipped from custom_nodes.comfyui-kjnodes with essentials_category set
looked like a core essentials node, and the v2 search Essentials chip
plus organizeAllNodes pulled it (and its pack-prefixed category folder)
into the Essentials view alongside core nodes.

Drop the essentials_category branch from getNodeSource so
nodeSource.type tracks only the python_module. The dedicated Essentials
tab and the v2 search Essentials chip now share resolveEssentialsCategory
(already gated on isCoreNode) as the source of truth.

- isCustomNode is correct again for custom-pack-essentials nodes
- organizeAllNodes no longer needs the Essentials union; Core suffices
- groupNodesByPack no longer needs the essentials_category arg
- nodeDefStore no longer threads essentials_category through getNodeSource
- removed unused NodeSourceType.Essentials, isEssentialNode

Adds:
- Unit regression tests in NodeSearchContent.test.ts covering both
  Essentials and Extensions chips with mixed core/custom-pack nodes
- E2E spec nodeSearchBoxV2Essentials.spec.ts that mocks object_info
  with custom-pack-essentials and core-essentials fixtures and asserts
  the chips classify them correctly

Fixes FE-568
2026-06-19 14:40:18 -07:00
10 changed files with 319 additions and 66 deletions

View 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

View 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)
})
}
)

View File

@@ -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) => {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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({

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)
)