mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-25 23:25:02 +00:00
Backport of #11493 to `cloud/1.44` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12130-backport-cloud-1-44-fix-Load-Image-preview-retains-deleted-asset-FE-230-35d6d73d3650812983cfdd3086dc6969) by [Unito](https://www.unito.io) Co-authored-by: Dante <bunggl@naver.com>
242 lines
6.6 KiB
TypeScript
242 lines
6.6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
|
|
import {
|
|
clearNodePreviewCacheForValues,
|
|
findNodesReferencingValues
|
|
} from './clearNodePreviewCacheForValues'
|
|
|
|
type MockWidget = { name: string; value: unknown }
|
|
type MockNode = {
|
|
id: number
|
|
widgets?: MockWidget[]
|
|
imgs?: unknown
|
|
videoContainer?: unknown
|
|
graph?: { setDirtyCanvas: (v: boolean) => void }
|
|
isSubgraphNode?: () => boolean
|
|
subgraph?: { nodes: MockNode[] }
|
|
}
|
|
|
|
function makeGraph(nodes: MockNode[]): LGraph {
|
|
return { nodes } as unknown as LGraph
|
|
}
|
|
|
|
describe('FE-230 clearNodePreviewCacheForValues', () => {
|
|
it('clears node.imgs and removes outputs when a widget value matches a deleted value', () => {
|
|
const setDirty = vi.fn()
|
|
const remove = vi.fn()
|
|
const node: MockNode = {
|
|
id: 7,
|
|
widgets: [{ name: 'image', value: 'foo.png' }],
|
|
imgs: [{ src: 'blob:stale' }],
|
|
graph: { setDirtyCanvas: setDirty }
|
|
}
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([node]),
|
|
new Set(['foo.png']),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(node.imgs).toBeUndefined()
|
|
expect(remove).toHaveBeenCalledWith(node)
|
|
expect(setDirty).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
it('leaves unrelated nodes untouched', () => {
|
|
const setDirty = vi.fn()
|
|
const remove = vi.fn()
|
|
const node: MockNode = {
|
|
id: 8,
|
|
widgets: [{ name: 'image', value: 'unrelated.png' }],
|
|
imgs: [{ src: 'blob:keep' }],
|
|
graph: { setDirtyCanvas: setDirty }
|
|
}
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([node]),
|
|
new Set(['foo.png']),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(node.imgs).toEqual([{ src: 'blob:keep' }])
|
|
expect(remove).not.toHaveBeenCalled()
|
|
expect(setDirty).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('no-ops when the deleted value set is empty', () => {
|
|
const setDirty = vi.fn()
|
|
const remove = vi.fn()
|
|
const node: MockNode = {
|
|
id: 9,
|
|
widgets: [{ name: 'image', value: 'foo.png' }],
|
|
imgs: [{ src: 'blob:keep' }],
|
|
graph: { setDirtyCanvas: setDirty }
|
|
}
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([node]),
|
|
new Set(),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(node.imgs).toEqual([{ src: 'blob:keep' }])
|
|
expect(remove).not.toHaveBeenCalled()
|
|
expect(setDirty).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('matches the [output]-annotated form for output assets', () => {
|
|
const remove = vi.fn()
|
|
const node: MockNode = {
|
|
id: 12,
|
|
widgets: [{ name: 'image', value: 'foo.png [output]' }],
|
|
imgs: [{ src: 'blob:stale' }],
|
|
graph: { setDirtyCanvas: vi.fn() }
|
|
}
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([node]),
|
|
new Set(['foo.png [output]']),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(node.imgs).toBeUndefined()
|
|
expect(remove).toHaveBeenCalledWith(node)
|
|
})
|
|
|
|
it('matches the subfolder-prefixed annotated form when provided', () => {
|
|
const remove = vi.fn()
|
|
const node: MockNode = {
|
|
id: 13,
|
|
widgets: [{ name: 'image', value: 'sub/foo.png [output]' }],
|
|
imgs: [{ src: 'blob:stale' }],
|
|
graph: { setDirtyCanvas: vi.fn() }
|
|
}
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([node]),
|
|
new Set(['sub/foo.png [output]']),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(node.imgs).toBeUndefined()
|
|
expect(remove).toHaveBeenCalledWith(node)
|
|
})
|
|
|
|
it('does not cross-match basenames across input/output sources', () => {
|
|
const remove = vi.fn()
|
|
const inputNode: MockNode = {
|
|
id: 1,
|
|
widgets: [{ name: 'image', value: 'foo.png' }],
|
|
imgs: [{ src: 'blob:input' }],
|
|
graph: { setDirtyCanvas: vi.fn() }
|
|
}
|
|
const outputNode: MockNode = {
|
|
id: 2,
|
|
widgets: [{ name: 'image', value: 'foo.png [output]' }],
|
|
imgs: [{ src: 'blob:output' }],
|
|
graph: { setDirtyCanvas: vi.fn() }
|
|
}
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([inputNode, outputNode]),
|
|
new Set(['foo.png']),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(inputNode.imgs).toBeUndefined()
|
|
expect(outputNode.imgs).toEqual([{ src: 'blob:output' }])
|
|
expect(remove).toHaveBeenCalledWith(inputNode)
|
|
expect(remove).not.toHaveBeenCalledWith(outputNode)
|
|
})
|
|
|
|
it('also clears videoContainer for video previews', () => {
|
|
const remove = vi.fn()
|
|
const node: MockNode = {
|
|
id: 15,
|
|
widgets: [{ name: 'video', value: 'clip.mp4' }],
|
|
videoContainer: { foo: 'bar' },
|
|
graph: { setDirtyCanvas: vi.fn() }
|
|
}
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([node]),
|
|
new Set(['clip.mp4']),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(node.videoContainer).toBeUndefined()
|
|
expect(remove).toHaveBeenCalledWith(node)
|
|
})
|
|
|
|
it('matches any widget on the node, not just "image"', () => {
|
|
const remove = vi.fn()
|
|
const node: MockNode = {
|
|
id: 10,
|
|
widgets: [
|
|
{ name: 'seed', value: 42 },
|
|
{ name: 'video', value: 'clip.mp4' }
|
|
],
|
|
imgs: [{ src: 'blob:videostale' }],
|
|
graph: { setDirtyCanvas: vi.fn() }
|
|
}
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([node]),
|
|
new Set(['clip.mp4']),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(node.imgs).toBeUndefined()
|
|
expect(remove).toHaveBeenCalledWith(node)
|
|
})
|
|
|
|
it('walks subgraph interiors and matches nested nodes', () => {
|
|
const inner: MockNode = {
|
|
id: 100,
|
|
widgets: [{ name: 'image', value: 'nested.png [output]' }],
|
|
imgs: [{ src: 'blob:nested' }],
|
|
graph: { setDirtyCanvas: vi.fn() }
|
|
}
|
|
const wrapper: MockNode = {
|
|
id: 50,
|
|
widgets: [],
|
|
isSubgraphNode: () => true,
|
|
subgraph: { nodes: [inner] }
|
|
}
|
|
const remove = vi.fn()
|
|
|
|
clearNodePreviewCacheForValues(
|
|
makeGraph([wrapper]),
|
|
new Set(['nested.png [output]']),
|
|
remove as unknown as (node: LGraphNode) => void
|
|
)
|
|
|
|
expect(inner.imgs).toBeUndefined()
|
|
expect(remove).toHaveBeenCalledWith(inner)
|
|
})
|
|
})
|
|
|
|
describe('FE-230 findNodesReferencingValues', () => {
|
|
it('skips subgraph wrapper nodes (only their interior nodes match)', () => {
|
|
const inner: MockNode = {
|
|
id: 100,
|
|
widgets: [{ name: 'image', value: 'foo.png' }]
|
|
}
|
|
const wrapper: MockNode = {
|
|
id: 50,
|
|
widgets: [{ name: 'image', value: 'foo.png' }],
|
|
isSubgraphNode: () => true,
|
|
subgraph: { nodes: [inner] }
|
|
}
|
|
|
|
const matches = findNodesReferencingValues(
|
|
makeGraph([wrapper]),
|
|
new Set(['foo.png'])
|
|
)
|
|
|
|
expect(matches).toEqual([inner])
|
|
})
|
|
})
|