mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-04 04:30:04 +00:00
## Summary Resolves six open issues by reorganizing node replacement components into a domain-driven folder structure, refactoring event handling to follow the emit pattern, and adding comprehensive test coverage across all affected modules. ## Changes - **What**: - Moved `SwapNodeGroupRow.vue` and `SwapNodesCard.vue` from `src/components/rightSidePanel/errors/` to `src/platform/nodeReplacement/components/` (Issues #9255) - Moved `useMissingNodeScan.ts` from `src/composables/` to `src/platform/nodeReplacement/missingNodeScan.ts`, renamed to reflect it is a plain function not a Vue composable (Issues #9254) - Refactored `SwapNodeGroupRow.vue` to emit a `'replace'` event instead of calling `useNodeReplacement()` and `useExecutionErrorStore()` directly; replacement logic now handled in `TabErrors.vue` (Issue #9267) - Added unit tests for `removeMissingNodesByType` (`executionErrorStore.test.ts`), `scanMissingNodes` (`missingNodeScan.test.ts`), and `swapNodeGroups` computed (`swapNodeGroups.test.ts`, `useErrorGroups.test.ts`) (Issue #9270) - Added placeholder detection tests covering unregistered-type detection when `has_errors` is false, and exclusion of registered types (`useNodeReplacement.test.ts`) (Issue #9271) - Added component tests for `MissingNodeCard` and `MissingPackGroupRow` covering rendering, expand/collapse, events, install states, and edge cases (Issue #9231) - Added component tests for `SwapNodeGroupRow` and `SwapNodesCard` (Issues #9255, #9267) ## Additional Changes (Post-Review) - **Edge case guard in placeholder detection** (`useNodeReplacement.ts`): When `last_serialization.type` is absent (old serialization format), the predicate falls back to `n.type`, which the app may have already run through `sanitizeNodeName` — stripping HTML special characters (`& < > " ' \` =`). In that case, a `Set.has()` lookup against the original unsanitized type name would silently miss, causing replacement to be skipped. Fixed by including sanitized variants of each target type in the `targetTypes` Set at construction time. For the overwhelmingly common case (no special characters in type names), the Set deduplicates the entries and runtime behavior is identical to before. A regression test was added to cover the specific scenario: `last_serialization.type` absent + live `n.type` already sanitized. ## Review Focus - `TabErrors.vue`: confirm the new `@replace` event handler correctly replaces nodes and removes them from missing nodes list (mirrors the old inline logic in `SwapNodeGroupRow`) - `missingNodeScan.ts`: filename/export name change from `useMissingNodeScan` — verify all call sites updated via `app.ts` - Test mocking strategy: module-level `vi.mock()` factories use closures over `ref`/plain objects to allow per-test overrides without global mutable state - Fixes #9231 - Fixes #9254 - Fixes #9255 - Fixes #9267 - Fixes #9270 - Fixes #9271
160 lines
5.0 KiB
TypeScript
160 lines
5.0 KiB
TypeScript
import { createPinia, setActivePinia } from 'pinia'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { MissingNodeType } from '@/types/comfy'
|
|
|
|
// Mock dependencies
|
|
vi.mock('@/i18n', () => ({
|
|
st: vi.fn((_key: string, fallback: string) => fallback)
|
|
}))
|
|
|
|
vi.mock('@/platform/distribution/types', () => ({
|
|
isCloud: false
|
|
}))
|
|
|
|
vi.mock('@/stores/settingStore', () => ({
|
|
useSettingStore: vi.fn(() => ({
|
|
get: vi.fn(() => false)
|
|
}))
|
|
}))
|
|
|
|
import { useExecutionErrorStore } from './executionErrorStore'
|
|
|
|
describe('executionErrorStore — missing node operations', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
})
|
|
|
|
describe('setMissingNodeTypes', () => {
|
|
it('sets missingNodesError with provided types', () => {
|
|
const store = useExecutionErrorStore()
|
|
const types: MissingNodeType[] = [
|
|
{ type: 'NodeA', nodeId: '1', isReplaceable: false }
|
|
]
|
|
store.setMissingNodeTypes(types)
|
|
|
|
expect(store.missingNodesError).not.toBeNull()
|
|
expect(store.missingNodesError?.nodeTypes).toHaveLength(1)
|
|
expect(store.hasMissingNodes).toBe(true)
|
|
})
|
|
|
|
it('clears missingNodesError when given empty array', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
{ type: 'NodeA', nodeId: '1', isReplaceable: false }
|
|
])
|
|
expect(store.missingNodesError).not.toBeNull()
|
|
|
|
store.setMissingNodeTypes([])
|
|
expect(store.missingNodesError).toBeNull()
|
|
expect(store.hasMissingNodes).toBe(false)
|
|
})
|
|
|
|
it('deduplicates string entries by value', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
'NodeA',
|
|
'NodeA',
|
|
'NodeB'
|
|
] as MissingNodeType[])
|
|
|
|
expect(store.missingNodesError?.nodeTypes).toHaveLength(2)
|
|
})
|
|
|
|
it('deduplicates object entries by nodeId when present', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
{ type: 'NodeA', nodeId: '1', isReplaceable: false },
|
|
{ type: 'NodeA', nodeId: '1', isReplaceable: false },
|
|
{ type: 'NodeA', nodeId: '2', isReplaceable: false }
|
|
])
|
|
|
|
// Same nodeId='1' deduplicated, nodeId='2' kept
|
|
expect(store.missingNodesError?.nodeTypes).toHaveLength(2)
|
|
})
|
|
|
|
it('deduplicates object entries by type when nodeId is absent', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
{ type: 'NodeA', isReplaceable: false },
|
|
{ type: 'NodeA', isReplaceable: true }
|
|
] as MissingNodeType[])
|
|
|
|
// Same type, no nodeId → deduplicated
|
|
expect(store.missingNodesError?.nodeTypes).toHaveLength(1)
|
|
})
|
|
|
|
it('keeps distinct nodeIds even when type is the same', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
{ type: 'NodeA', nodeId: '1', isReplaceable: false },
|
|
{ type: 'NodeA', nodeId: '2', isReplaceable: false },
|
|
{ type: 'NodeA', nodeId: '3', isReplaceable: false }
|
|
])
|
|
|
|
expect(store.missingNodesError?.nodeTypes).toHaveLength(3)
|
|
})
|
|
})
|
|
|
|
describe('removeMissingNodesByType', () => {
|
|
it('removes matching types from the missing nodes list', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
{ type: 'NodeA', nodeId: '1', isReplaceable: false },
|
|
{ type: 'NodeB', nodeId: '2', isReplaceable: false },
|
|
{ type: 'NodeC', nodeId: '3', isReplaceable: false }
|
|
])
|
|
|
|
store.removeMissingNodesByType(['NodeA', 'NodeC'])
|
|
|
|
expect(store.missingNodesError?.nodeTypes).toHaveLength(1)
|
|
const remaining = store.missingNodesError?.nodeTypes[0]
|
|
expect(typeof remaining !== 'string' && remaining?.type).toBe('NodeB')
|
|
})
|
|
|
|
it('clears missingNodesError when all types are removed', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
{ type: 'NodeA', nodeId: '1', isReplaceable: false }
|
|
])
|
|
|
|
store.removeMissingNodesByType(['NodeA'])
|
|
|
|
expect(store.missingNodesError).toBeNull()
|
|
expect(store.hasMissingNodes).toBe(false)
|
|
})
|
|
|
|
it('does nothing when missingNodesError is null', () => {
|
|
const store = useExecutionErrorStore()
|
|
expect(store.missingNodesError).toBeNull()
|
|
|
|
// Should not throw
|
|
store.removeMissingNodesByType(['NodeA'])
|
|
expect(store.missingNodesError).toBeNull()
|
|
})
|
|
|
|
it('does nothing when removing non-existent types', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
{ type: 'NodeA', nodeId: '1', isReplaceable: false }
|
|
])
|
|
|
|
store.removeMissingNodesByType(['NonExistent'])
|
|
|
|
expect(store.missingNodesError?.nodeTypes).toHaveLength(1)
|
|
})
|
|
|
|
it('handles removing from string entries', () => {
|
|
const store = useExecutionErrorStore()
|
|
store.setMissingNodeTypes([
|
|
'StringNodeA',
|
|
'StringNodeB'
|
|
] as MissingNodeType[])
|
|
|
|
store.removeMissingNodesByType(['StringNodeA'])
|
|
|
|
expect(store.missingNodesError?.nodeTypes).toHaveLength(1)
|
|
})
|
|
})
|
|
})
|