Compare commits

..

1 Commits

Author SHA1 Message Date
CodeRabbit Fixer
69bc33eef5 fix: Add unit tests for resolveSubgraphInputLink (#9293)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:04:25 +01:00
8 changed files with 72 additions and 62 deletions

View File

@@ -2,13 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
const { mockFetchApi, mockAddAlert, mockUpdateInputs, mockSetNodeUploading } =
vi.hoisted(() => ({
mockFetchApi: vi.fn(),
mockAddAlert: vi.fn(),
mockUpdateInputs: vi.fn(),
mockSetNodeUploading: vi.fn()
}))
const { mockFetchApi, mockAddAlert, mockUpdateInputs } = vi.hoisted(() => ({
mockFetchApi: vi.fn(),
mockAddAlert: vi.fn(),
mockUpdateInputs: vi.fn()
}))
let capturedDragOnDrop: (files: File[]) => Promise<string[]>
@@ -45,10 +43,6 @@ vi.mock('@/stores/assetsStore', () => ({
useAssetsStore: () => ({ updateInputs: mockUpdateInputs })
}))
vi.mock('@/stores/nodeOutputStore', () => ({
useNodeOutputStore: () => ({ setNodeUploading: mockSetNodeUploading })
}))
function createMockNode(): LGraphNode {
return {
isUploading: false,

View File

@@ -7,7 +7,6 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import type { ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useAssetsStore } from '@/stores/assetsStore'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
const PASTED_IMAGE_EXPIRY_MS = 2000
@@ -93,15 +92,12 @@ export const useNodeImageUpload = (
}
}
const nodeOutputStore = useNodeOutputStore()
const handleUploadBatch = async (files: File[]) => {
if (node.isUploading) {
useToastStore().addAlert(t('g.uploadAlreadyInProgress'))
return []
}
node.isUploading = true
nodeOutputStore.setNodeUploading(node.id, true)
try {
node.imgs = undefined
@@ -118,7 +114,6 @@ export const useNodeImageUpload = (
return validPaths
} finally {
node.isUploading = false
nodeOutputStore.setNodeUploading(node.id, false)
node.graph?.setDirtyCanvas(true)
}
}

View File

@@ -121,6 +121,68 @@ describe('resolveSubgraphInputLink', () => {
expect(result).toBe('seed_input')
})
test('skips broken links where getLink returns undefined', () => {
const { subgraph, subgraphNode } = createSubgraphSetup('prompt')
addLinkedInteriorInput(subgraph, 'prompt', 'valid_input', 'valid')
const broken = addLinkedInteriorInput(
subgraph,
'prompt',
'broken_input',
'broken'
)
const originalGetLink = subgraph.getLink.bind(subgraph)
vi.spyOn(subgraph, 'getLink').mockImplementation((linkId) => {
if (typeof linkId !== 'number') return originalGetLink(linkId)
if (linkId === broken.linkId) return undefined
return originalGetLink(linkId)
})
const result = resolveSubgraphInputLink(
subgraphNode,
'prompt',
({ targetInput }) => targetInput.name
)
expect(result).toBe('valid_input')
})
test('returns result from latest connection when multiple links resolve', () => {
const { subgraph, subgraphNode } = createSubgraphSetup('prompt')
addLinkedInteriorInput(subgraph, 'prompt', 'older_input', 'older')
addLinkedInteriorInput(subgraph, 'prompt', 'newer_input', 'newer')
const result = resolveSubgraphInputLink(
subgraphNode,
'prompt',
({ targetInput }) => targetInput.name
)
expect(result).toBe('newer_input')
})
test('falls back to earlier link when latest resolve callback returns undefined', () => {
const { subgraph, subgraphNode } = createSubgraphSetup('prompt')
addLinkedInteriorInput(subgraph, 'prompt', 'fallback_input', 'fallback')
const newer = addLinkedInteriorInput(
subgraph,
'prompt',
'skipped_input',
'skipped'
)
const result = resolveSubgraphInputLink(
subgraphNode,
'prompt',
({ targetInput }) => {
if (targetInput.link === newer.linkId) return undefined
return targetInput.name
}
)
expect(result).toBe('fallback_input')
})
test('caches getTargetWidget result within the same callback evaluation', () => {
const { subgraph, subgraphNode } = createSubgraphSetup('model')
const linked = addLinkedInteriorInput(

View File

@@ -176,7 +176,6 @@
"control_before_generate": "control before generate",
"choose_file_to_upload": "choose file to upload",
"uploadAlreadyInProgress": "Upload already in progress",
"uploading": "Uploading",
"capture": "capture",
"nodes": "Nodes",
"nodesCount": "{count} node | {count} nodes",

View File

@@ -37,7 +37,7 @@
</div>
<!-- Main Image -->
<img
v-if="!imageError && !isUploading"
v-if="!imageError"
:src="currentImageUrl"
:alt="imageAltText"
class="pointer-events-none absolute inset-0 block size-full object-contain"
@@ -45,16 +45,6 @@
@error="handleImageError"
/>
<!-- Upload Spinner Overlay -->
<div
v-if="isUploading"
class="absolute inset-0 flex items-center justify-center"
>
<i
class="icon-[lucide--loader-circle] size-8 animate-spin text-base-foreground"
/>
</div>
<!-- Floating Action Buttons (appear on hover and focus) -->
<div
v-if="isHovered || isFocused"
@@ -95,10 +85,7 @@
<!-- Image Dimensions -->
<div class="pt-2 text-center text-xs text-base-foreground">
<span v-if="isUploading" class="text-base-foreground">
{{ $t('g.uploading') }}...
</span>
<span v-else-if="imageError" class="text-red-400">
<span v-if="imageError" class="text-red-400">
{{ $t('g.errorLoadingImage') }}
</span>
<span v-else-if="showLoader" class="text-base-foreground">
@@ -147,8 +134,6 @@ interface ImagePreviewProps {
readonly imageUrls: readonly string[]
/** Optional node ID for context-aware actions */
readonly nodeId?: string
/** Whether a file is being uploaded to this node */
readonly isUploading?: boolean
}
const props = defineProps<ImagePreviewProps>()

View File

@@ -122,7 +122,6 @@
v-if="nodeMedia"
:node-data="nodeData"
:media="nodeMedia"
:is-uploading="isNodeUploading"
/>
<NodeContent
v-for="preview in promotedPreviews"
@@ -745,8 +744,6 @@ const nodeMedia = computed(() => {
return { type, urls } as const
})
const isNodeUploading = computed(() => nodeOutputs.isNodeUploading(nodeData.id))
const nodeContainerRef = ref<HTMLDivElement>()
// Drag and drop support

View File

@@ -20,7 +20,6 @@
v-else-if="hasMedia && media?.type === 'image'"
:image-urls="media.urls"
:node-id="nodeId"
:is-uploading="isUploading"
class="mt-2 flex-auto"
/>
</slot>
@@ -44,15 +43,14 @@ interface NodeContentProps {
type: 'image' | 'video' | 'audio'
urls: string[]
}
isUploading?: boolean
}
const { nodeData, media, isUploading = false } = defineProps<NodeContentProps>()
const props = defineProps<NodeContentProps>()
const hasMedia = computed(() => media && media.urls.length > 0)
const hasMedia = computed(() => props.media && props.media.urls.length > 0)
// Get node ID from nodeData
const nodeId = computed(() => nodeData?.id?.toString())
const nodeId = computed(() => props.nodeData?.id?.toString())
// Error boundary implementation
const renderError = ref<string | null>(null)

View File

@@ -48,22 +48,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
const scheduledRevoke: Record<NodeLocatorId, { stop: () => void }> = {}
const latestPreview = ref<string[]>([])
const uploadingNodeIds = ref(new Set<string>())
function setNodeUploading(nodeId: string | number, uploading: boolean) {
const id = String(nodeId)
if (uploading) {
uploadingNodeIds.value.add(id)
} else {
uploadingNodeIds.value.delete(id)
}
uploadingNodeIds.value = new Set(uploadingNodeIds.value)
}
function isNodeUploading(nodeId: string | number): boolean {
return uploadingNodeIds.value.has(String(nodeId))
}
function scheduleRevoke(locator: NodeLocatorId, cb: () => void) {
scheduledRevoke[locator]?.stop()
@@ -479,10 +463,6 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
restoreOutputs,
resetAllOutputsAndPreviews,
// Upload state
setNodeUploading,
isNodeUploading,
// State
nodeOutputs,
nodePreviewImages,