Files
ComfyUI_frontend/src/stores/appModeStore.test.ts
pythongosssss 68b16e3a3f feat: App mode saving rework (#9338)
## Summary

Change app mode changes to be written directly to the workflow on change
instead of requiring explicit save via builder.
Temporary: Adds `.app.json` file extension to app files for
identification since we don't currently have a way to identify them with
metadata
Removes app builder save dialog and replaces it with default mode
selection

## Changes

- **What**: 
- ensure all save locations handle app mode
- remove dirtyLinearData and flushing

- **Breaking**: 
- if people are relying on workflow names and are converting to/from app
mode in the same workflow, they will gain/lose the `.app` part of the
extension

## Screenshots (if applicable)

<img width="689" height="84" alt="image"
src="https://github.com/user-attachments/assets/335596ee-dce9-4e3a-a7b5-f0715c294e41"
/>

<img width="421" height="324" alt="image"
src="https://github.com/user-attachments/assets/ad3cd33c-e9f0-4c30-8874-d4507892fc6b"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9338-feat-App-mode-saving-rework-3176d73d3650813f9ae1f6c5a234da8c)
by [Unito](https://www.unito.io)
2026-03-03 11:35:36 -08:00

164 lines
4.8 KiB
TypeScript

import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { nextTick } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LoadedComfyWorkflow } from '@/platform/workflow/management/stores/comfyWorkflow'
import { ComfyWorkflow as ComfyWorkflowClass } from '@/platform/workflow/management/stores/comfyWorkflow'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
vi.mock('@/scripts/app', () => ({
app: {
rootGraph: { extra: {} }
}
}))
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({
getCanvas: () => ({ read_only: false })
})
}))
import { useAppModeStore } from './appModeStore'
function createBuilderWorkflow(
activeMode: string = 'builder:select'
): LoadedComfyWorkflow {
const workflow = new ComfyWorkflowClass({
path: 'workflows/test.json',
modified: Date.now(),
size: 100
})
workflow.changeTracker = createMockChangeTracker()
workflow.content = '{}'
workflow.originalContent = '{}'
workflow.activeMode = activeMode as LoadedComfyWorkflow['activeMode']
return workflow as LoadedComfyWorkflow
}
describe('appModeStore', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
vi.mocked(app.rootGraph).extra = {}
})
describe('enterBuilder', () => {
it('navigates to builder:arrange when in app mode with outputs', () => {
const workflowStore = useWorkflowStore()
workflowStore.activeWorkflow = createBuilderWorkflow('app')
const store = useAppModeStore()
store.selectedOutputs.push(1)
store.enterBuilder()
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:arrange')
})
it('navigates to builder:select when in app mode without outputs', () => {
const workflowStore = useWorkflowStore()
workflowStore.activeWorkflow = createBuilderWorkflow('app')
const store = useAppModeStore()
store.enterBuilder()
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
})
it('navigates to builder:select when in graph mode with outputs', () => {
const workflowStore = useWorkflowStore()
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
const store = useAppModeStore()
store.selectedOutputs.push(1)
store.enterBuilder()
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
})
it('navigates to builder:select when in graph mode without outputs', () => {
const workflowStore = useWorkflowStore()
workflowStore.activeWorkflow = createBuilderWorkflow('graph')
const store = useAppModeStore()
store.enterBuilder()
expect(workflowStore.activeWorkflow!.activeMode).toBe('builder:select')
})
})
describe('linearData sync watcher', () => {
it('writes linearData to rootGraph.extra when in builder mode', async () => {
const workflowStore = useWorkflowStore()
const store = useAppModeStore()
workflowStore.activeWorkflow = createBuilderWorkflow()
await nextTick()
store.selectedOutputs.push(1)
await nextTick()
expect(app.rootGraph.extra.linearData).toEqual({
inputs: [],
outputs: [1]
})
})
it('does not write linearData when not in builder mode', async () => {
const workflowStore = useWorkflowStore()
const store = useAppModeStore()
const workflow = createBuilderWorkflow()
workflow.activeMode = 'graph'
workflowStore.activeWorkflow = workflow
await nextTick()
store.selectedOutputs.push(1)
await nextTick()
expect(app.rootGraph.extra.linearData).toBeUndefined()
})
it('does not write when rootGraph is null', async () => {
const workflowStore = useWorkflowStore()
const store = useAppModeStore()
workflowStore.activeWorkflow = createBuilderWorkflow()
await nextTick()
const originalRootGraph = app.rootGraph
Object.defineProperty(app, 'rootGraph', { value: null, writable: true })
store.selectedOutputs.push(1)
await nextTick()
Object.defineProperty(app, 'rootGraph', {
value: originalRootGraph,
writable: true
})
})
it('reflects input changes in linearData', async () => {
const workflowStore = useWorkflowStore()
const store = useAppModeStore()
workflowStore.activeWorkflow = createBuilderWorkflow()
await nextTick()
store.selectedInputs.push([42, 'prompt'])
await nextTick()
expect(app.rootGraph.extra.linearData).toEqual({
inputs: [[42, 'prompt']],
outputs: []
})
})
})
})