Compare commits

...

2 Commits

Author SHA1 Message Date
Glary-Bot
a122079bfe 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-05-19 13:08:46 +00:00
Glary-Bot
918cd8b57b 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-05-13 20:55:06 +00:00
8 changed files with 156 additions and 66 deletions

View File

@@ -0,0 +1,84 @@
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 }) => {
test.setTimeout(60_000)
await comfyPage.page.route('**/api/object_info', (route) =>
route.fulfill({ json: fixtureDefs })
)
await comfyPage.workflow.reloadAndWaitForApp()
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

@@ -125,15 +125,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)
)