Compare commits

...

1 Commits

Author SHA1 Message Date
huang47
8d26b1e0cd refactor: remove dead branches and tidy store cleanups 2026-06-30 22:38:00 -07:00
7 changed files with 354 additions and 27 deletions

View File

@@ -0,0 +1,26 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
const electronAPI = vi.hoisted(() => vi.fn())
vi.mock('@/platform/distribution/types', () => ({ isDesktop: false }))
vi.mock('@/utils/envUtil', () => ({ electronAPI }))
describe('electronDownloadStore outside desktop', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
electronAPI.mockClear()
})
it('skips the Electron bridge when not running on desktop', async () => {
const store = useElectronDownloadStore()
await store.initialize()
expect(electronAPI).not.toHaveBeenCalled()
expect(store.downloads).toEqual([])
})
})

View File

@@ -0,0 +1,106 @@
import { DownloadStatus } from '@comfyorg/comfyui-electron-types'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
const downloadManagerMock = vi.hoisted(() => ({
cancelDownload: vi.fn(),
getAllDownloads: vi.fn(),
onDownloadProgress: vi.fn(),
pauseDownload: vi.fn(),
resumeDownload: vi.fn(),
startDownload: vi.fn()
}))
vi.mock('@/platform/distribution/types', () => ({
isDesktop: true
}))
vi.mock('@/utils/envUtil', () => ({
electronAPI: () => ({
DownloadManager: downloadManagerMock
})
}))
const flushPromises = () =>
new Promise<void>((resolve) => setTimeout(resolve, 0))
describe('electronDownloadStore', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
Object.values(downloadManagerMock).forEach((mock) => mock.mockReset())
downloadManagerMock.getAllDownloads.mockResolvedValue([
{
filename: 'done.bin',
status: DownloadStatus.COMPLETED,
url: 'https://example.com/done.bin'
}
])
})
it('loads existing downloads and applies progress updates by URL', async () => {
let progressCallback:
| Parameters<typeof downloadManagerMock.onDownloadProgress>[0]
| undefined
downloadManagerMock.onDownloadProgress.mockImplementation((callback) => {
progressCallback = callback
})
const store = useElectronDownloadStore()
await flushPromises()
progressCallback?.({
filename: 'model.bin',
progress: 25,
savePath: '/tmp/model.bin',
status: DownloadStatus.IN_PROGRESS,
url: 'https://example.com/model.bin'
})
progressCallback?.({
filename: 'model.bin',
progress: 50,
savePath: '/tmp/model.bin',
status: DownloadStatus.IN_PROGRESS,
url: 'https://example.com/model.bin'
})
expect(store.findByUrl('https://example.com/done.bin')?.status).toBe(
DownloadStatus.COMPLETED
)
expect(store.findByUrl('https://example.com/model.bin')).toMatchObject({
filename: 'model.bin',
progress: 50,
status: DownloadStatus.IN_PROGRESS
})
expect(store.inProgressDownloads).toHaveLength(1)
})
it('delegates download controls to the Electron bridge', async () => {
const store = useElectronDownloadStore()
await store.start({
filename: 'model.bin',
savePath: '/tmp/model.bin',
url: 'https://example.com/model.bin'
})
await store.pause('https://example.com/model.bin')
await store.resume('https://example.com/model.bin')
await store.cancel('https://example.com/model.bin')
expect(downloadManagerMock.startDownload).toHaveBeenCalledWith(
'https://example.com/model.bin',
'/tmp/model.bin',
'model.bin'
)
expect(downloadManagerMock.pauseDownload).toHaveBeenCalledWith(
'https://example.com/model.bin'
)
expect(downloadManagerMock.resumeDownload).toHaveBeenCalledWith(
'https://example.com/model.bin'
)
expect(downloadManagerMock.cancelDownload).toHaveBeenCalledWith(
'https://example.com/model.bin'
)
})
})

View File

@@ -33,18 +33,15 @@ export const useElectronDownloadStore = defineStore('downloads', () => {
}
DownloadManager.onDownloadProgress((data) => {
if (!findByUrl(data.url)) {
downloads.value.push(data)
}
const download = findByUrl(data.url)
if (download) {
download.progress = data.progress
download.status = data.status
download.filename = data.filename
download.savePath = data.savePath
if (!download) {
downloads.value.push(data)
return
}
download.progress = data.progress
download.status = data.status
download.filename = data.filename
download.savePath = data.savePath
})
}

View File

@@ -105,8 +105,12 @@ export class ComfyNodeDefImpl
* @internal
* Migrate default input options to forceInput.
*/
private static _migrateDefaultInput(nodeDef: ComfyNodeDefV1): ComfyNodeDefV1 {
const def = _.cloneDeep(nodeDef)
private static _migrateDefaultInput(
nodeDef: ComfyNodeDefV1
): ComfyNodeDefV1 & { input: ComfyInputSpecV1 } {
const def = _.cloneDeep(nodeDef) as ComfyNodeDefV1 & {
input: ComfyInputSpecV1
}
def.input ??= {}
// For required inputs, now we have the input socket always present. Specifying
// it now has no effect.
@@ -156,7 +160,7 @@ export class ComfyNodeDefImpl
this.dev_only = obj.dev_only ?? false
this.output_node = obj.output_node
this.api_node = !!obj.api_node
this.input = obj.input ?? {}
this.input = obj.input
this.output = obj.output ?? []
this.output_is_list = obj.output_is_list
this.output_name = obj.output_name

View File

@@ -273,9 +273,6 @@ export class TaskItemImpl {
}
calculateFlatOutputs(): ReadonlyArray<ResultItemImpl> {
if (!this.outputs) {
return []
}
return parseTaskOutput(this.outputs)
}
@@ -435,9 +432,6 @@ export class TaskItemImpl {
// Use full outputs from job detail, or fall back to existing outputs
const outputsToLoad = jobDetail?.outputs ?? this.outputs
if (!outputsToLoad) {
return
}
const nodeOutputsStore = useNodeOutputStore()
const rawOutputs = toRaw(outputsToLoad)

View File

@@ -1,7 +1,21 @@
import { describe, expect, it } from 'vitest'
import type { TreeNode } from '@/types/treeExplorerTypes'
import { buildTree, sortedTree } from '@/utils/treeUtil'
import {
buildTree,
combineTrees,
findNodeByKey,
flattenTree,
sortedTree,
unwrapTreeRoot
} from '@/utils/treeUtil'
const createNode = (label: string, leaf = false): TreeNode => ({
key: label,
label,
leaf,
children: []
})
describe('buildTree', () => {
it('should handle empty folder items correctly', () => {
@@ -65,14 +79,101 @@ describe('buildTree', () => {
})
})
describe('sortedTree', () => {
const createNode = (label: string, leaf = false): TreeNode => ({
key: label,
label,
leaf,
children: []
describe('unwrapTreeRoot', () => {
it('promotes the single non-leaf folder child', () => {
const tree: TreeNode = {
key: 'root',
label: 'root',
children: [
{
key: 'root/a',
label: 'a',
leaf: false,
children: [createNode('child', true)]
}
]
}
expect(unwrapTreeRoot(tree).children?.map((node) => node.key)).toEqual([
'child'
])
})
it('keeps roots with leaf, empty, or multiple children intact', () => {
const leafRoot: TreeNode = {
key: 'root',
label: 'root',
children: [createNode('leaf', true)]
}
const emptyFolderRoot: TreeNode = {
key: 'root',
label: 'root',
children: [createNode('folder')]
}
const multiRoot: TreeNode = {
key: 'root',
label: 'root',
children: [createNode('a'), createNode('b')]
}
const childWithoutChildren: TreeNode = {
key: 'root',
label: 'root',
children: [
{
key: 'root/a',
label: 'a',
leaf: false
}
]
}
expect(unwrapTreeRoot(leafRoot)).toBe(leafRoot)
expect(unwrapTreeRoot(emptyFolderRoot)).toBe(emptyFolderRoot)
expect(unwrapTreeRoot(multiRoot)).toBe(multiRoot)
expect(unwrapTreeRoot(childWithoutChildren)).toBe(childWithoutChildren)
})
})
describe('flattenTree', () => {
it('returns data from leaf nodes only', () => {
const tree: TreeNode = {
key: 'root',
label: 'root',
children: [
{
key: 'folder',
label: 'folder',
children: [
{
key: 'leaf-a',
label: 'leaf-a',
leaf: true,
data: { path: 'a' }
},
{
key: 'leaf-b',
label: 'leaf-b',
leaf: true
}
]
},
{
key: 'leaf-c',
label: 'leaf-c',
leaf: true,
data: { path: 'c' }
}
]
}
expect(flattenTree<{ path: string }>(tree)).toEqual([
{ path: 'c' },
{ path: 'a' }
])
})
})
describe('sortedTree', () => {
it('should return a new node instance', () => {
const node = createNode('root')
const result = sortedTree(node)
@@ -92,6 +193,25 @@ describe('sortedTree', () => {
expect(result.children?.map((c) => c.label)).toEqual(['a', 'b', 'c'])
})
it('sorts children with missing labels by the empty-label fallback', () => {
const unlabeled = {
key: 'missing',
label: undefined as unknown as string,
leaf: true
}
const node: TreeNode = {
key: 'root',
label: 'root',
leaf: false,
children: [unlabeled, createNode('a', true)]
}
expect(sortedTree(node).children?.map((c) => c.key)).toEqual([
'missing',
'a'
])
})
describe('with groupLeaf=true', () => {
it('should group folders before files', () => {
const node: TreeNode = {
@@ -110,6 +230,35 @@ describe('sortedTree', () => {
expect(labels).toEqual(['folder1', 'folder2', 'another.txt', 'file.txt'])
})
it('sorts grouped children with missing labels', () => {
const unlabeledFolder = {
key: 'folder-missing',
label: undefined as unknown as string,
leaf: false,
children: []
}
const unlabeledFile = {
key: 'file-missing',
label: undefined as unknown as string,
leaf: true,
children: []
}
const node: TreeNode = {
key: 'root',
label: 'root',
children: [
createNode('folder-b'),
unlabeledFolder,
createNode('file-b', true),
unlabeledFile
]
}
expect(
sortedTree(node, { groupLeaf: true }).children?.map((c) => c.key)
).toEqual(['folder-missing', 'folder-b', 'file-missing', 'file-b'])
})
it('should sort recursively', () => {
const node: TreeNode = {
key: 'root',
@@ -145,3 +294,54 @@ describe('sortedTree', () => {
expect(result).toEqual(node)
})
})
describe('findNodeByKey', () => {
it('returns the matching nested node or null', () => {
const child = createNode('root/child')
const tree: TreeNode = {
key: 'root',
label: 'root',
children: [child]
}
expect(findNodeByKey(tree, 'root')).toBe(tree)
expect(findNodeByKey(tree, 'root/child')).toBe(child)
expect(findNodeByKey(tree, 'missing')).toBeNull()
expect(findNodeByKey(createNode('root'), 'missing')).toBeNull()
})
})
describe('combineTrees', () => {
it('adds a cloned subtree under its matching parent', () => {
const root: TreeNode = {
key: 'root',
label: 'root',
children: [{ key: 'root/a', label: 'a', children: [] }]
}
const subtree: TreeNode = {
key: 'root/a/b',
label: 'b',
leaf: true,
data: { path: 'b' }
}
const combined = combineTrees(root, subtree)
expect(combined).not.toBe(root)
expect(combined.children?.[0].children?.[0]).toEqual(subtree)
expect(combined.children?.[0].children?.[0]).not.toBe(subtree)
expect(root.children?.[0].children).toEqual([])
})
it('returns a clone unchanged when the parent key is absent', () => {
const root: TreeNode = { key: 'root', label: 'root' }
const combined = combineTrees(root, {
key: 'root/missing/leaf',
label: 'leaf',
leaf: true
})
expect(combined).toEqual(root)
expect(combined).not.toBe(root)
})
})

View File

@@ -148,7 +148,7 @@ function cloneTree<T extends TreeNode>(node: T): T {
const clone = { ...node }
// Clone children recursively
if (node.children && node.children.length > 0) {
if (node.children) {
clone.children = node.children.map((child) => cloneTree(child))
}