mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-11 01:28:03 +00:00
Compare commits
2 Commits
codex/cove
...
codex/cove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e617228f51 | ||
|
|
994ec7ba6f |
@@ -12,6 +12,7 @@ import {
|
||||
Reroute,
|
||||
SubgraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink'
|
||||
import type { SerialisableGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
import { zeroUuid } from '@/utils/uuid'
|
||||
@@ -127,6 +128,31 @@ describe('LGraph', () => {
|
||||
const fromOldSchema = new LGraph(oldSchemaGraph)
|
||||
expect(fromOldSchema).toMatchSnapshot('oldSchemaGraph')
|
||||
})
|
||||
|
||||
test('skips null entries in v0.4 links arrays during configure()', ({
|
||||
expect,
|
||||
oldSchemaGraph
|
||||
}) => {
|
||||
const graph = new LGraph()
|
||||
const validLink: SerialisedLLinkArray = [
|
||||
1,
|
||||
toNodeId(1),
|
||||
0,
|
||||
toNodeId(2),
|
||||
0,
|
||||
'number'
|
||||
]
|
||||
|
||||
expect(() =>
|
||||
graph.configure({
|
||||
...oldSchemaGraph,
|
||||
links: [null, validLink]
|
||||
})
|
||||
).not.toThrow()
|
||||
|
||||
expect(graph._links.size).toBe(1)
|
||||
expect(graph._links.get(toLinkId(1))).toBeInstanceOf(LLink)
|
||||
})
|
||||
subgraphTest('should snap slots to same y-level', ({ emptySubgraph }) => {
|
||||
const node = new LGraphNode('testname')
|
||||
node.addInput('test', 'IMAGE')
|
||||
|
||||
47
src/renderer/extensions/linearMode/PartnerNodesList.test.ts
Normal file
47
src/renderer/extensions/linearMode/PartnerNodesList.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { usePriceBadge } from '@/composables/node/usePriceBadge'
|
||||
import { createTestRootGraph } from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import PartnerNodesList from '@/renderer/extensions/linearMode/PartnerNodesList.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: { linearMode: { hasCreditCost: 'Has credit cost' } }
|
||||
}
|
||||
})
|
||||
|
||||
function renderList(mobile = false) {
|
||||
const pinia = createTestingPinia({ stubActions: false })
|
||||
setActivePinia(pinia)
|
||||
|
||||
return render(PartnerNodesList, {
|
||||
props: { mobile },
|
||||
global: { plugins: [pinia, i18n] }
|
||||
})
|
||||
}
|
||||
|
||||
describe('PartnerNodesList', () => {
|
||||
it('renders badges for nodes in the root graph even when app.graph points at a subgraph', () => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
const { getCreditsBadge } = usePriceBadge()
|
||||
const rootGraph = createTestRootGraph()
|
||||
const apiNode = new LGraphNode('Api Node')
|
||||
apiNode.badges = [getCreditsBadge('$0.05/Run')]
|
||||
rootGraph.add(apiNode)
|
||||
|
||||
Reflect.set(app, 'rootGraphInternal', rootGraph)
|
||||
|
||||
renderList()
|
||||
|
||||
expect(screen.getByText('$0.05/Run')).toBeInTheDocument()
|
||||
expect(screen.getByText('Api Node')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
fetchWithUnifiedRemint,
|
||||
@@ -65,6 +65,10 @@ describe('ComfyApi cloud mode', () => {
|
||||
vi.stubGlobal('WebSocket', FakeWebSocket)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('adds cloud auth headers and enables unified retry for authenticated requests', async () => {
|
||||
mockAuthStore.getAuthHeader.mockResolvedValue({
|
||||
Authorization: 'Bearer firebase-token'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import type { PartialDeep } from '@total-typescript/shoehorn'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
LGraph,
|
||||
@@ -437,6 +437,10 @@ describe('ComfyApp', () => {
|
||||
singletonApp.nodeOutputs = {}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('queuePrompt', () => {
|
||||
it('queues the request but does not start a second processor while busy', async () => {
|
||||
const dispatchCustomEvent = vi
|
||||
@@ -2576,6 +2580,27 @@ describe('ComfyApp', () => {
|
||||
})
|
||||
|
||||
describe('drop handler', () => {
|
||||
const registeredDropListeners: EventListener[] = []
|
||||
|
||||
function installDropHandler() {
|
||||
const addEventListener = vi.spyOn(document, 'addEventListener')
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
const [, dropListener] = addEventListener.mock.calls.find(
|
||||
([type]) => type === 'drop'
|
||||
)!
|
||||
addEventListener.mockRestore()
|
||||
registeredDropListeners.push(dropListener as EventListener)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const listener of registeredDropListeners) {
|
||||
document.removeEventListener('drop', listener)
|
||||
}
|
||||
registeredDropListeners.length = 0
|
||||
})
|
||||
|
||||
it('syncs graph_mouse and routes mixed file drops by media type', async () => {
|
||||
const graphMouse: [number, number] = [-999, -999]
|
||||
const adjustMouseEvent = vi.fn((e: DragEvent) => {
|
||||
@@ -2602,9 +2627,7 @@ describe('ComfyApp', () => {
|
||||
vi.spyOn(app, 'handleVideoFileList').mockResolvedValue(undefined)
|
||||
vi.spyOn(app, 'handleFile').mockResolvedValue(undefined)
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
|
||||
document.dispatchEvent(new DragEvent('drop'))
|
||||
await flushDropHandler()
|
||||
@@ -2640,9 +2663,7 @@ describe('ComfyApp', () => {
|
||||
vi.spyOn(app, 'handleVideoFileList').mockResolvedValue(undefined)
|
||||
vi.spyOn(app, 'handleFile').mockResolvedValue(undefined)
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
document.dispatchEvent(new DragEvent('drop'))
|
||||
await flushDropHandler()
|
||||
|
||||
@@ -2668,9 +2689,7 @@ describe('ComfyApp', () => {
|
||||
mockExtractFilesFromDragEvent.mockResolvedValue([workflow])
|
||||
const handleFile = vi.spyOn(app, 'handleFile').mockResolvedValue()
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
document.dispatchEvent(new DragEvent('drop'))
|
||||
await flushDropHandler()
|
||||
|
||||
@@ -2689,9 +2708,7 @@ describe('ComfyApp', () => {
|
||||
mockExtractFilesFromDragEvent.mockResolvedValue([])
|
||||
const handleFile = vi.spyOn(app, 'handleFile').mockResolvedValue()
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
document.dispatchEvent(new DragEvent('drop'))
|
||||
await flushDropHandler()
|
||||
|
||||
@@ -2707,9 +2724,7 @@ describe('ComfyApp', () => {
|
||||
})
|
||||
mockExtractFilesFromDragEvent.mockRejectedValue(new Error('drop failed'))
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
document.dispatchEvent(new DragEvent('drop'))
|
||||
await flushDropHandler()
|
||||
|
||||
@@ -2720,9 +2735,7 @@ describe('ComfyApp', () => {
|
||||
|
||||
it('ignores drops that were already handled by nested targets', async () => {
|
||||
const handleFile = vi.spyOn(app, 'handleFile').mockResolvedValue()
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
|
||||
const event = new DragEvent('drop', { cancelable: true })
|
||||
event.preventDefault()
|
||||
@@ -2749,9 +2762,7 @@ describe('ComfyApp', () => {
|
||||
})
|
||||
const handleFile = vi.spyOn(app, 'handleFile').mockResolvedValue()
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
document.dispatchEvent(new DragEvent('drop'))
|
||||
await flushDropHandler()
|
||||
|
||||
@@ -2788,9 +2799,7 @@ describe('ComfyApp', () => {
|
||||
return 1
|
||||
})
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
canvasEl.dispatchEvent(new DragEvent('dragover'))
|
||||
canvasEl.dispatchEvent(new DragEvent('dragleave'))
|
||||
|
||||
@@ -2821,9 +2830,7 @@ describe('ComfyApp', () => {
|
||||
})
|
||||
})
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
canvasEl.dispatchEvent(new DragEvent('dragover'))
|
||||
|
||||
expect(app.dragOverNode).toBeNull()
|
||||
@@ -2834,9 +2841,7 @@ describe('ComfyApp', () => {
|
||||
app.canvasElRef.value = canvasEl
|
||||
app.dragOverNode = null
|
||||
|
||||
;(
|
||||
Object.getPrototypeOf(app) as { addDropHandler(): void }
|
||||
).addDropHandler.call(app)
|
||||
installDropHandler()
|
||||
canvasEl.dispatchEvent(new DragEvent('dragleave'))
|
||||
|
||||
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
|
||||
|
||||
@@ -189,13 +189,12 @@ describe('ComfyApp', () => {
|
||||
})
|
||||
|
||||
describe('graph (deprecated getter)', () => {
|
||||
it('returns undefined before the root graph is initialized', () => {
|
||||
it('returns undefined until the root graph is initialized', () => {
|
||||
expect(app.graph).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns the root graph once initialized', () => {
|
||||
const graph = new LGraph()
|
||||
Reflect.set(app, 'rootGraphInternal', graph)
|
||||
|
||||
expect(app.graph).toBe(graph)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -490,6 +490,17 @@ describe('ChangeTracker', () => {
|
||||
tracker.updateModified()
|
||||
expect(workflow.isModified).toBe(true)
|
||||
})
|
||||
|
||||
it('does nothing when the workflow cannot be resolved from the store', () => {
|
||||
const tracker = createTracker(createState(1))
|
||||
mockWorkflowStore.getWorkflowByPath.mockReturnValue(null)
|
||||
|
||||
expect(() => tracker.updateModified()).not.toThrow()
|
||||
expect(api.dispatchCustomEvent).toHaveBeenCalledWith(
|
||||
'graphChanged',
|
||||
tracker.activeState
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('undo and redo', () => {
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('DOMWidgetImpl', () => {
|
||||
expect(isComponentWidget(domWidget)).toBe(false)
|
||||
})
|
||||
|
||||
test('uses option-backed values, callbacks, and margins', () => {
|
||||
test('uses option-backed values, callbacks, and margins over defaults', () => {
|
||||
const node = new LGraphNode('test-node')
|
||||
let value = 'initial'
|
||||
const setValue = vi.fn((next: string) => {
|
||||
@@ -149,6 +149,13 @@ describe('DOMWidgetImpl', () => {
|
||||
}
|
||||
})
|
||||
widget.callback = callback
|
||||
const defaultWidget = new DOMWidgetImpl({
|
||||
node,
|
||||
name: 'text-default',
|
||||
type: 'text',
|
||||
element: document.createElement('textarea'),
|
||||
options: {}
|
||||
})
|
||||
|
||||
widget.value = 'next'
|
||||
|
||||
@@ -156,20 +163,8 @@ describe('DOMWidgetImpl', () => {
|
||||
expect(widget.margin).toBe(4)
|
||||
expect(setValue).toHaveBeenCalledWith('next')
|
||||
expect(callback).toHaveBeenCalledWith('next')
|
||||
})
|
||||
|
||||
test('uses default value and margin when options do not provide them', () => {
|
||||
const node = new LGraphNode('test-node')
|
||||
const widget = new DOMWidgetImpl({
|
||||
node,
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
element: document.createElement('textarea'),
|
||||
options: {}
|
||||
})
|
||||
|
||||
expect(widget.value).toBe('')
|
||||
expect(widget.margin).toBe(10)
|
||||
expect(defaultWidget.value).toBe('')
|
||||
expect(defaultWidget.margin).toBe(10)
|
||||
})
|
||||
|
||||
test('draws zoom placeholders and delegates visible draws', () => {
|
||||
@@ -297,7 +292,7 @@ describe('DOMWidgetImpl', () => {
|
||||
registerWidget.mockClear()
|
||||
unregisterWidget.mockClear()
|
||||
const node = new LGraphNode('test-node')
|
||||
node.graph = {} as LGraph
|
||||
node.graph = fromPartial<LGraph>({})
|
||||
const beforeResize = vi.fn()
|
||||
const afterResize = vi.fn()
|
||||
const widget = new DOMWidgetImpl({
|
||||
|
||||
@@ -94,9 +94,12 @@ describe('getWorkflowDataFromFile', () => {
|
||||
|
||||
it('routes isobmff by mime type and by file extension', async () => {
|
||||
await getWorkflowDataFromFile(file('video/mp4'))
|
||||
await getWorkflowDataFromFile(file('', 'clip.mp4'))
|
||||
await getWorkflowDataFromFile(file('', 'clip.mov'))
|
||||
await getWorkflowDataFromFile(file('', 'clip.m4v'))
|
||||
expect(getFromIsobmffFile).toHaveBeenCalledTimes(3)
|
||||
await getWorkflowDataFromFile(file('video/quicktime'))
|
||||
await getWorkflowDataFromFile(file('video/x-m4v'))
|
||||
expect(getFromIsobmffFile).toHaveBeenCalledTimes(6)
|
||||
})
|
||||
|
||||
it('routes svg and gltf by mime type or extension', async () => {
|
||||
|
||||
@@ -149,7 +149,7 @@ import {
|
||||
type MockWidget = IBaseWidget
|
||||
|
||||
function makeTargetWidget(overrides: Partial<MockWidget> = {}): MockWidget {
|
||||
return {
|
||||
return fromPartial<MockWidget>({
|
||||
name: 'seed',
|
||||
value: 1,
|
||||
callback: vi.fn(),
|
||||
@@ -157,7 +157,7 @@ function makeTargetWidget(overrides: Partial<MockWidget> = {}): MockWidget {
|
||||
linkedWidgets: [],
|
||||
computedDisabled: false,
|
||||
...overrides
|
||||
} as MockWidget
|
||||
})
|
||||
}
|
||||
|
||||
function makeNode(inputs: LGraphNode['inputs'] = []) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DEFAULT_DARK_COLOR_PALETTE } from '@/constants/coreColorPalettes'
|
||||
import {
|
||||
@@ -128,6 +128,10 @@ describe('useColorPaletteService', () => {
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('adds valid custom palettes and persists the custom palette map', async () => {
|
||||
const service = useColorPaletteService()
|
||||
|
||||
|
||||
@@ -364,6 +364,11 @@ function createMockWidget(
|
||||
})
|
||||
}
|
||||
|
||||
const originalClipboardDescriptor = Object.getOwnPropertyDescriptor(
|
||||
navigator,
|
||||
'clipboard'
|
||||
)
|
||||
|
||||
function installClipboard(write: Clipboard['write']) {
|
||||
class TestClipboardItem {
|
||||
constructor(readonly data: Record<string, Blob>) {}
|
||||
@@ -377,6 +382,14 @@ function installClipboard(write: Clipboard['write']) {
|
||||
return TestClipboardItem
|
||||
}
|
||||
|
||||
function restoreClipboard() {
|
||||
if (originalClipboardDescriptor) {
|
||||
Object.defineProperty(navigator, 'clipboard', originalClipboardDescriptor)
|
||||
} else {
|
||||
Reflect.deleteProperty(navigator, 'clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
function createInputSpec(overrides: Partial<InputSpec> = {}): InputSpec {
|
||||
return {
|
||||
name: 'prompt',
|
||||
@@ -459,6 +472,7 @@ describe('litegraphService', () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
restoreClipboard()
|
||||
})
|
||||
|
||||
describe('getExtraOptionsForWidget', () => {
|
||||
@@ -494,13 +508,13 @@ describe('litegraphService', () => {
|
||||
expect(options[0].content).toContain('My Label')
|
||||
})
|
||||
|
||||
it('calls toggleFavorite when favorite option callback is invoked', () => {
|
||||
it('calls toggleFavorite when favorite option callback is invoked', async () => {
|
||||
const node = createMockNode()
|
||||
const widget = createMockWidget()
|
||||
|
||||
const options = getExtraOptionsForWidget(node, widget)
|
||||
|
||||
void invokeMenuCallback(options[0])
|
||||
await invokeMenuCallback(options[0])
|
||||
expect(mockFavoritedWidgetsStore.toggleFavorite).toHaveBeenCalledWith(
|
||||
node,
|
||||
'test_widget'
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useMediaCache } from './mediaCacheService'
|
||||
|
||||
const NativeURL = URL
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn()
|
||||
global.URL = fromPartial<typeof URL>({
|
||||
createObjectURL: vi.fn(() => 'blob:mock-url'),
|
||||
revokeObjectURL: vi.fn()
|
||||
})
|
||||
|
||||
describe('mediaCacheService', () => {
|
||||
describe('URL reference counting', () => {
|
||||
it('should handle URL acquisition for non-existent cache entry', () => {
|
||||
|
||||
@@ -524,6 +524,32 @@ describe('legacy workflow migration helpers', () => {
|
||||
expect(subgraph?.links?.[0].target_slot).toBe(0)
|
||||
})
|
||||
|
||||
it('retargets the valid link when links contains a null entry', () => {
|
||||
const graph = fromPartial<ISerialisedGraph>({
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'Node',
|
||||
inputs: [
|
||||
{
|
||||
name: 'widget',
|
||||
type: 'STRING',
|
||||
link: null,
|
||||
widget: { name: 'w' }
|
||||
},
|
||||
{ name: 'kept', type: 'STRING', link: 7 }
|
||||
]
|
||||
}
|
||||
],
|
||||
links: [null, [7, 2, 0, 1, 99, 'STRING']]
|
||||
})
|
||||
|
||||
compressWidgetInputSlots(graph)
|
||||
|
||||
expect(graph.nodes[0].inputs?.map((input) => input.name)).toEqual(['kept'])
|
||||
expect(graph.links[1]?.[4]).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps labeled widget inputs and tolerates missing links', () => {
|
||||
const graph = fromPartial<ISerialisedGraph>({
|
||||
nodes: [
|
||||
|
||||
Reference in New Issue
Block a user