mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 13:32:11 +00:00
## Summary Manual backport of #12111 to `cloud/1.44`. This suppresses false-positive missing media detection while media loader nodes are still uploading files from drag/drop, paste, or file-select flows. ## Conflict Resolution The cherry-pick conflicted only in `src/platform/missingMedia/missingMediaScan.test.ts` because the target branch still has the older annotated-media parameterized test block around the insertion point. I resolved it by: - adding the new upload-state tests from #12111 above the existing annotated-media cases - keeping the existing release-branch annotated-media `it.each` cases intact - using `it.for([false, true])` only for the new upload-state test added by #12111 ## Validation - `pnpm install --frozen-lockfile` - `pnpm exec vitest run src/platform/missingMedia/missingMediaScan.test.ts src/composables/node/useNodeImageUpload.test.ts src/extensions/core/uploadAudio.test.ts src/composables/graph/useErrorClearingHooks.test.ts` Result: 4 files passed, 87 tests passed. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12189-backport-cloud-1-44-fix-suppress-missing-media-scan-during-uploads-12111-35e6d73d36508195a407f1fa0d6898e7) by [Unito](https://www.unito.io)
184 lines
5.0 KiB
TypeScript
184 lines
5.0 KiB
TypeScript
import { fromAny } from '@total-typescript/shoehorn'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import type { ResultItem } from '@/schemas/apiSchema'
|
|
|
|
const { mockFetchApi, mockAddAlert, mockUpdateInputs } = vi.hoisted(() => ({
|
|
mockFetchApi: vi.fn(),
|
|
mockAddAlert: vi.fn(),
|
|
mockUpdateInputs: vi.fn()
|
|
}))
|
|
|
|
let capturedDragOnDrop: (files: File[]) => Promise<string[]>
|
|
|
|
vi.mock('@/composables/node/useNodeDragAndDrop', () => ({
|
|
useNodeDragAndDrop: (
|
|
_node: LGraphNode,
|
|
opts: { onDrop: typeof capturedDragOnDrop }
|
|
) => {
|
|
capturedDragOnDrop = opts.onDrop
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/composables/node/useNodeFileInput', () => ({
|
|
useNodeFileInput: () => ({ openFileSelection: vi.fn() })
|
|
}))
|
|
|
|
vi.mock('@/composables/node/useNodePaste', () => ({
|
|
useNodePaste: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@/i18n', () => ({
|
|
t: (key: string) => key
|
|
}))
|
|
|
|
vi.mock('@/platform/updates/common/toastStore', () => ({
|
|
useToastStore: () => ({ addAlert: mockAddAlert })
|
|
}))
|
|
|
|
vi.mock('@/scripts/api', () => ({
|
|
api: { fetchApi: mockFetchApi }
|
|
}))
|
|
|
|
vi.mock('@/stores/assetsStore', () => ({
|
|
useAssetsStore: () => ({ updateInputs: mockUpdateInputs })
|
|
}))
|
|
|
|
function createMockNode(): LGraphNode {
|
|
return fromAny<LGraphNode, unknown>({
|
|
isUploading: false,
|
|
imgs: [new Image()],
|
|
graph: { setDirtyCanvas: vi.fn() },
|
|
size: [300, 400]
|
|
})
|
|
}
|
|
|
|
function createFile(name = 'test.png', type = 'image/png'): File {
|
|
return new File(['data'], name, { type })
|
|
}
|
|
|
|
function successResponse(name: string, subfolder?: string) {
|
|
return {
|
|
status: 200,
|
|
json: () => Promise.resolve({ name, subfolder })
|
|
}
|
|
}
|
|
|
|
function failResponse(status = 500) {
|
|
return {
|
|
status,
|
|
statusText: 'Server Error'
|
|
}
|
|
}
|
|
|
|
describe('useNodeImageUpload', () => {
|
|
let node: LGraphNode
|
|
let onUploadComplete: (paths: (string | ResultItem)[]) => void
|
|
let onUploadStart: (files: File[]) => void
|
|
let onUploadError: () => void
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
vi.clearAllMocks()
|
|
node = createMockNode()
|
|
onUploadComplete = vi.fn()
|
|
onUploadStart = vi.fn()
|
|
onUploadError = vi.fn()
|
|
|
|
const { useNodeImageUpload } = await import('./useNodeImageUpload')
|
|
useNodeImageUpload(node, {
|
|
onUploadComplete,
|
|
onUploadStart,
|
|
onUploadError,
|
|
folder: 'input'
|
|
})
|
|
})
|
|
|
|
it.for([
|
|
{ mediaType: 'image', filename: 'test.png', mimeType: 'image/png' },
|
|
{ mediaType: 'video', filename: 'clip.mp4', mimeType: 'video/mp4' }
|
|
])(
|
|
'sets isUploading true during $mediaType upload and false after',
|
|
async ({ filename, mimeType }) => {
|
|
mockFetchApi.mockResolvedValueOnce(successResponse(filename))
|
|
|
|
const promise = capturedDragOnDrop([createFile(filename, mimeType)])
|
|
expect(node.isUploading).toBe(true)
|
|
|
|
await promise
|
|
expect(node.isUploading).toBe(false)
|
|
}
|
|
)
|
|
|
|
it('clears node.imgs on upload start', async () => {
|
|
mockFetchApi.mockResolvedValueOnce(successResponse('test.png'))
|
|
|
|
const promise = capturedDragOnDrop([createFile()])
|
|
expect(node.imgs).toBeUndefined()
|
|
|
|
await promise
|
|
})
|
|
|
|
it('calls onUploadStart with files', async () => {
|
|
mockFetchApi.mockResolvedValueOnce(successResponse('test.png'))
|
|
const files = [createFile()]
|
|
|
|
await capturedDragOnDrop(files)
|
|
expect(onUploadStart).toHaveBeenCalledWith(files)
|
|
})
|
|
|
|
it('calls onUploadComplete with valid paths on success', async () => {
|
|
mockFetchApi.mockResolvedValueOnce(successResponse('test.png'))
|
|
|
|
await capturedDragOnDrop([createFile()])
|
|
expect(onUploadComplete).toHaveBeenCalledWith(['test.png'])
|
|
})
|
|
|
|
it('includes subfolder in returned path', async () => {
|
|
mockFetchApi.mockResolvedValueOnce(successResponse('test.png', 'pasted'))
|
|
|
|
await capturedDragOnDrop([createFile()])
|
|
expect(onUploadComplete).toHaveBeenCalledWith(['pasted/test.png'])
|
|
})
|
|
|
|
it('calls onUploadError when all uploads fail', async () => {
|
|
mockFetchApi.mockResolvedValueOnce(failResponse())
|
|
|
|
await capturedDragOnDrop([createFile()])
|
|
expect(onUploadError).toHaveBeenCalled()
|
|
expect(onUploadComplete).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('resets isUploading even when upload fails', async () => {
|
|
mockFetchApi.mockRejectedValueOnce(new Error('Network error'))
|
|
|
|
await capturedDragOnDrop([createFile()])
|
|
expect(node.isUploading).toBe(false)
|
|
})
|
|
|
|
it('rejects concurrent uploads with a toast', async () => {
|
|
mockFetchApi.mockImplementation(
|
|
() =>
|
|
new Promise((resolve) =>
|
|
setTimeout(() => resolve(successResponse('a.png')), 50)
|
|
)
|
|
)
|
|
|
|
const first = capturedDragOnDrop([createFile('a.png')])
|
|
const second = await capturedDragOnDrop([createFile('b.png')])
|
|
|
|
expect(second).toEqual([])
|
|
expect(mockAddAlert).toHaveBeenCalledWith('g.uploadAlreadyInProgress')
|
|
|
|
await first
|
|
})
|
|
|
|
it('calls setDirtyCanvas on start and finish', async () => {
|
|
mockFetchApi.mockResolvedValueOnce(successResponse('test.png'))
|
|
|
|
await capturedDragOnDrop([createFile()])
|
|
expect(node.graph?.setDirtyCanvas).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|