mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-07 15:47:53 +00:00
Compare commits
1 Commits
chore/code
...
shihchi/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d26b1e0cd |
26
src/stores/electronDownloadStore.nonDesktop.test.ts
Normal file
26
src/stores/electronDownloadStore.nonDesktop.test.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
106
src/stores/electronDownloadStore.test.ts
Normal file
106
src/stores/electronDownloadStore.test.ts
Normal 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'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user