mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-13 11:11:00 +00:00
Compare commits
3 Commits
feature/lo
...
fix/progre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbdbaf02d9 | ||
|
|
0db8d1a2e5 | ||
|
|
12b352f220 |
@@ -840,8 +840,52 @@ describe('SubgraphNode.widgets getter', () => {
|
||||
subgraphNode.id
|
||||
)
|
||||
expect(promotions).toStrictEqual([
|
||||
{ sourceNodeId: String(linkedNodeA.id), sourceWidgetName: 'string_a' },
|
||||
{ sourceNodeId: String(independentNode.id), sourceWidgetName: 'string_a' }
|
||||
{
|
||||
sourceNodeId: String(independentNode.id),
|
||||
sourceWidgetName: 'string_a'
|
||||
},
|
||||
{ sourceNodeId: String(linkedNodeA.id), sourceWidgetName: 'string_a' }
|
||||
])
|
||||
})
|
||||
|
||||
test('syncPromotions preserves user-reordered store order', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'widget_a', type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 97 })
|
||||
subgraphNode.graph?.add(subgraphNode)
|
||||
|
||||
const linkedNode = new LGraphNode('LinkedNode')
|
||||
const linkedInput = linkedNode.addInput('widget_a', '*')
|
||||
linkedNode.addWidget('text', 'widget_a', 'val_a', () => {})
|
||||
linkedInput.widget = { name: 'widget_a' }
|
||||
subgraph.add(linkedNode)
|
||||
subgraph.inputNode.slots[0].connect(linkedInput, linkedNode)
|
||||
|
||||
const independentNode = new LGraphNode('IndependentNode')
|
||||
independentNode.addWidget('text', 'indep_widget', 'val_b', () => {})
|
||||
subgraph.add(independentNode)
|
||||
|
||||
// Simulate user reorder: independent first, then linked
|
||||
setPromotions(subgraphNode, [
|
||||
[String(independentNode.id), 'indep_widget'],
|
||||
[String(linkedNode.id), 'widget_a']
|
||||
])
|
||||
|
||||
callSyncPromotions(subgraphNode)
|
||||
|
||||
const promotions = usePromotionStore().getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
|
||||
// Order must be preserved: independent first, linked second
|
||||
expect(promotions).toStrictEqual([
|
||||
{
|
||||
sourceNodeId: String(independentNode.id),
|
||||
sourceWidgetName: 'indep_widget'
|
||||
},
|
||||
{ sourceNodeId: String(linkedNode.id), sourceWidgetName: 'widget_a' }
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const { displayNameByViewKey, reconcileEntries } =
|
||||
this._buildPromotionReconcileState(entries, linkedEntries)
|
||||
|
||||
const views = this._promotedViewManager.reconcile(
|
||||
const reconciledViews = this._promotedViewManager.reconcile(
|
||||
reconcileEntries,
|
||||
(entry) =>
|
||||
createPromotedWidgetView(
|
||||
@@ -287,6 +287,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
)
|
||||
)
|
||||
|
||||
const views = this._reorderViewsByStoreEntries(reconciledViews, entries)
|
||||
|
||||
this._promotedViewsCache = {
|
||||
version: this._cacheVersion,
|
||||
entriesRef: entries,
|
||||
@@ -385,13 +387,89 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
fallbackStoredEntries
|
||||
)
|
||||
|
||||
const desiredEntries = shouldPersistLinkedOnly
|
||||
? linkedPromotionEntries
|
||||
: [...linkedPromotionEntries, ...fallbackStoredEntries]
|
||||
|
||||
return {
|
||||
mergedEntries: shouldPersistLinkedOnly
|
||||
? linkedPromotionEntries
|
||||
: [...linkedPromotionEntries, ...fallbackStoredEntries]
|
||||
mergedEntries: this._orderPreservingMerge(entries, desiredEntries)
|
||||
}
|
||||
}
|
||||
|
||||
private _orderPreservingMerge(
|
||||
currentEntries: PromotedWidgetSource[],
|
||||
desiredEntries: PromotedWidgetSource[]
|
||||
): PromotedWidgetSource[] {
|
||||
const makeKey = (e: PromotedWidgetSource) =>
|
||||
this._makePromotionEntryKey(
|
||||
e.sourceNodeId,
|
||||
e.sourceWidgetName,
|
||||
e.disambiguatingSourceNodeId
|
||||
)
|
||||
|
||||
const desiredByKey = new Map(desiredEntries.map((e) => [makeKey(e), e]))
|
||||
const currentKeys = new Set(currentEntries.map(makeKey))
|
||||
|
||||
const preserved = currentEntries
|
||||
.filter((e) => desiredByKey.has(makeKey(e)))
|
||||
.map((e) => desiredByKey.get(makeKey(e))!)
|
||||
|
||||
const added = desiredEntries.filter((e) => !currentKeys.has(makeKey(e)))
|
||||
|
||||
return [...preserved, ...added]
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorders reconciled views to follow the current store entry order,
|
||||
* but only when the view set exactly matches the store entries (pure
|
||||
* reorder). When entries were added or removed (e.g. connection change),
|
||||
* the reconcile order is preserved.
|
||||
*/
|
||||
private _reorderViewsByStoreEntries(
|
||||
views: PromotedWidgetView[],
|
||||
storeEntries: PromotedWidgetSource[]
|
||||
): PromotedWidgetView[] {
|
||||
if (views.length <= 1 || storeEntries.length === 0) return views
|
||||
|
||||
const makeKey = (e: PromotedWidgetSource) =>
|
||||
this._makePromotionEntryKey(
|
||||
e.sourceNodeId,
|
||||
e.sourceWidgetName,
|
||||
e.disambiguatingSourceNodeId
|
||||
)
|
||||
|
||||
const storeKeys = new Set(storeEntries.map(makeKey))
|
||||
const viewKeys = new Set(views.map(makeKey))
|
||||
|
||||
if (storeKeys.size !== viewKeys.size) return views
|
||||
for (const key of viewKeys) {
|
||||
if (!storeKeys.has(key)) return views
|
||||
}
|
||||
|
||||
const viewsByKey = new Map<string, PromotedWidgetView[]>()
|
||||
for (const v of views) {
|
||||
const key = makeKey(v)
|
||||
const group = viewsByKey.get(key)
|
||||
if (group) group.push(v)
|
||||
else viewsByKey.set(key, [v])
|
||||
}
|
||||
|
||||
const emittedKeys = new Set<string>()
|
||||
const ordered: PromotedWidgetView[] = []
|
||||
|
||||
for (const entry of storeEntries) {
|
||||
const key = makeKey(entry)
|
||||
if (emittedKeys.has(key)) continue
|
||||
const group = viewsByKey.get(key)
|
||||
if (group) {
|
||||
ordered.push(...group)
|
||||
emittedKeys.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
return ordered
|
||||
}
|
||||
|
||||
private _collectLinkedAndFallbackEntries(
|
||||
entries: PromotedWidgetSource[],
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
|
||||
@@ -10,6 +10,7 @@ import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil'
|
||||
const mockNodeExecutionIdToNodeLocatorId = vi.fn()
|
||||
const mockNodeIdToNodeLocatorId = vi.fn()
|
||||
const mockNodeLocatorIdToNodeExecutionId = vi.fn()
|
||||
const mockShowTextPreview = vi.fn()
|
||||
|
||||
import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { NodeProgressState } from '@/schemas/apiSchema'
|
||||
@@ -38,7 +39,7 @@ declare global {
|
||||
|
||||
vi.mock('@/composables/node/useNodeProgressText', () => ({
|
||||
useNodeProgressText: () => ({
|
||||
showTextPreview: vi.fn()
|
||||
showTextPreview: mockShowTextPreview
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -431,6 +432,43 @@ describe('useExecutionStore - reconcileInitializingJobs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('useExecutionStore - progress_text startup guard', () => {
|
||||
let store: ReturnType<typeof useExecutionStore>
|
||||
|
||||
function fireProgressText(detail: {
|
||||
nodeId: string
|
||||
text: string
|
||||
prompt_id?: string
|
||||
}) {
|
||||
const handler = apiEventHandlers.get('progress_text')
|
||||
if (!handler) throw new Error('progress_text handler not bound')
|
||||
handler(new CustomEvent('progress_text', { detail }))
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
apiEventHandlers.clear()
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
store = useExecutionStore()
|
||||
store.bindExecutionEvents()
|
||||
})
|
||||
|
||||
it('should ignore progress_text before the canvas is initialized', async () => {
|
||||
const { useCanvasStore } =
|
||||
await import('@/renderer/core/canvas/canvasStore')
|
||||
useCanvasStore().canvas = null
|
||||
|
||||
expect(() =>
|
||||
fireProgressText({
|
||||
nodeId: '1',
|
||||
text: 'warming up'
|
||||
})
|
||||
).not.toThrow()
|
||||
|
||||
expect(mockShowTextPreview).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useExecutionErrorStore - Node Error Lookups', () => {
|
||||
let store: ReturnType<typeof useExecutionErrorStore>
|
||||
|
||||
|
||||
@@ -527,7 +527,10 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
// Handle execution node IDs for subgraphs
|
||||
const currentId = getNodeIdIfExecuting(nodeId)
|
||||
if (!currentId) return
|
||||
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
|
||||
const canvas = canvasStore.canvas
|
||||
if (!canvas) return
|
||||
|
||||
const node = canvas.graph?.getNodeById(currentId)
|
||||
if (!node) return
|
||||
|
||||
useNodeProgressText().showTextPreview(node, text)
|
||||
|
||||
Reference in New Issue
Block a user