mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-05 13:41:59 +00:00
App mode output feed to only show current session results for outputs defined in the app (#9307)
## Summary Updates app mode to only show images from: - the workflow that generated the image - in the current session - for the outputs selected in the builder ## Changes - **What**: - adds new mapping of jobid -> workflow path [cant use id here as it is not guaranteed unique], capped at 4k entries - fix bug where executing a workflow then quickly switching tabs associated incorrect workflow - add missing output history tests ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9307-App-mode-output-feed-to-only-show-current-session-results-for-outputs-defined-in-the-app-3156d73d36508142b4bbca3f938fc5c2) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useLinearOutputStore } from '@/renderer/extensions/linearMode/linearOutputStore'
|
||||
@@ -9,6 +9,9 @@ import { ResultItemImpl } from '@/stores/queueStore'
|
||||
const activeJobIdRef = ref<string | null>(null)
|
||||
const previewsRef = ref<Record<string, { url: string; nodeId?: string }>>({})
|
||||
const isAppModeRef = ref(true)
|
||||
const activeWorkflowPathRef = ref<string>('workflows/test-workflow.json')
|
||||
const jobIdToWorkflowPathRef = ref(new Map<string, string>())
|
||||
const selectedOutputsRef = ref<string[]>([])
|
||||
|
||||
const { apiTarget } = vi.hoisted(() => ({
|
||||
apiTarget: new EventTarget()
|
||||
@@ -20,10 +23,29 @@ vi.mock('@/composables/useAppMode', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/appModeStore', () => ({
|
||||
useAppModeStore: () => ({
|
||||
get selectedOutputs() {
|
||||
return selectedOutputsRef.value
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => ({
|
||||
get activeJobId() {
|
||||
return activeJobIdRef.value
|
||||
},
|
||||
get jobIdToSessionWorkflowPath() {
|
||||
return jobIdToWorkflowPathRef.value
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
get activeWorkflow() {
|
||||
return { path: activeWorkflowPathRef.value }
|
||||
}
|
||||
})
|
||||
}))
|
||||
@@ -59,6 +81,12 @@ vi.mock('@/renderer/extensions/linearMode/flattenNodeOutput', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
function setJobWorkflowPath(jobId: string, path: string) {
|
||||
const next = new Map(jobIdToWorkflowPathRef.value)
|
||||
next.set(jobId, path)
|
||||
jobIdToWorkflowPathRef.value = next
|
||||
}
|
||||
|
||||
function makeExecutedDetail(
|
||||
promptId: string,
|
||||
images: Array<Record<string, string>> = [
|
||||
@@ -80,11 +108,9 @@ describe('linearOutputStore', () => {
|
||||
activeJobIdRef.value = null
|
||||
previewsRef.value = {}
|
||||
isAppModeRef.value = true
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
activeJobIdRef.value = null
|
||||
previewsRef.value = {}
|
||||
activeWorkflowPathRef.value = 'workflows/test-workflow.json'
|
||||
jobIdToWorkflowPathRef.value = new Map()
|
||||
selectedOutputsRef.value = []
|
||||
})
|
||||
|
||||
it('creates a skeleton item when a job starts', () => {
|
||||
@@ -613,10 +639,105 @@ describe('linearOutputStore', () => {
|
||||
|
||||
expect(store.inProgressItems).toHaveLength(0)
|
||||
expect(store.selectedId).toBeNull()
|
||||
expect(store.trackedJobId).toBeNull()
|
||||
expect(store.pendingResolve.size).toBe(0)
|
||||
})
|
||||
|
||||
it('does not show in-progress items from another workflow', () => {
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
// Job-1 submitted from workflow-a
|
||||
activeWorkflowPathRef.value = 'workflows/app-a.json'
|
||||
setJobWorkflowPath('job-1', 'workflows/app-a.json')
|
||||
store.onJobStart('job-1')
|
||||
|
||||
// User switches to workflow-b: job-1 should NOT appear
|
||||
activeWorkflowPathRef.value = 'workflows/app-b.json'
|
||||
expect(store.activeWorkflowInProgressItems).toHaveLength(0)
|
||||
|
||||
// Back on workflow-a: job-1 should appear
|
||||
activeWorkflowPathRef.value = 'workflows/app-a.json'
|
||||
expect(store.activeWorkflowInProgressItems).toHaveLength(1)
|
||||
expect(store.activeWorkflowInProgressItems[0].jobId).toBe('job-1')
|
||||
})
|
||||
|
||||
it('uses executionStore path map for workflow scoping', () => {
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
// Simulate storeJob populating executionStore.jobIdToSessionWorkflowPath
|
||||
setJobWorkflowPath('job-1', 'workflows/app-a.json')
|
||||
setJobWorkflowPath('job-2', 'workflows/app-a.json')
|
||||
|
||||
// User switches to workflow-b before execution starts
|
||||
activeWorkflowPathRef.value = 'workflows/app-b.json'
|
||||
|
||||
store.onJobStart('job-1')
|
||||
store.onJobStart('job-2')
|
||||
|
||||
// On workflow-b: neither job should appear
|
||||
expect(store.activeWorkflowInProgressItems).toHaveLength(0)
|
||||
|
||||
// On workflow-a: both jobs should appear
|
||||
activeWorkflowPathRef.value = 'workflows/app-a.json'
|
||||
expect(store.activeWorkflowInProgressItems).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('scopes in-progress items per workflow with concurrent jobs', () => {
|
||||
vi.useFakeTimers()
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
// Job-1 on workflow-a (dog)
|
||||
activeWorkflowPathRef.value = 'workflows/app-a.json'
|
||||
setJobWorkflowPath('job-1', 'workflows/app-a.json')
|
||||
store.onJobStart('job-1')
|
||||
store.onLatentPreview('job-1', 'blob:dog')
|
||||
vi.advanceTimersByTime(16)
|
||||
|
||||
// User switches to workflow-b, runs job-2
|
||||
activeWorkflowPathRef.value = 'workflows/app-b.json'
|
||||
setJobWorkflowPath('job-2', 'workflows/app-b.json')
|
||||
|
||||
// Job-1 finishes, job-2 starts
|
||||
store.onJobComplete('job-1')
|
||||
store.onJobStart('job-2')
|
||||
store.onLatentPreview('job-2', 'blob:landscape')
|
||||
vi.advanceTimersByTime(16)
|
||||
|
||||
// On workflow-b: should only see job-2 (landscape), NOT job-1 (dog)
|
||||
const items = store.activeWorkflowInProgressItems
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].jobId).toBe('job-2')
|
||||
expect(items[0].latentPreviewUrl).toBe('blob:landscape')
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('skips output items for nodes not in selectedOutputs', () => {
|
||||
selectedOutputsRef.value = ['2']
|
||||
const store = useLinearOutputStore()
|
||||
store.onJobStart('job-1')
|
||||
|
||||
// Node 1 executes — not in selectedOutputs, should be skipped
|
||||
store.onNodeExecuted('job-1', makeExecutedDetail('job-1', undefined, '1'))
|
||||
|
||||
// Skeleton should still be there (not consumed by non-output node)
|
||||
expect(
|
||||
store.inProgressItems.filter((i) => i.state === 'image')
|
||||
).toHaveLength(0)
|
||||
|
||||
// Node 2 executes — in selectedOutputs, should create image item
|
||||
store.onNodeExecuted(
|
||||
'job-1',
|
||||
makeExecutedDetail(
|
||||
'job-1',
|
||||
[{ filename: 'out.png', subfolder: '', type: 'output' }],
|
||||
'2'
|
||||
)
|
||||
)
|
||||
|
||||
const imageItems = store.inProgressItems.filter((i) => i.state === 'image')
|
||||
expect(imageItems).toHaveLength(1)
|
||||
expect(imageItems[0].output?.nodeId).toBe('2')
|
||||
})
|
||||
|
||||
it('ignores execution events when not in app mode', async () => {
|
||||
const { nextTick } = await import('vue')
|
||||
const store = useLinearOutputStore()
|
||||
|
||||
Reference in New Issue
Block a user