Compare commits

...

2 Commits

Author SHA1 Message Date
huang47
e617228f51 test: address remaining review feedback on coverage tests
- Restore vi.restoreAllMocks() teardown in app.core.test.ts and
  api.cloud.test.ts/colorPaletteService.test.ts so console/prototype
  spies do not leak into later tests
- Fix accumulating document drop listeners across app.core.test.ts by
  capturing and removing each registered listener after every test
- Restore the navigator.clipboard descriptor in
  litegraphService.core.test.ts instead of relying on
  vi.unstubAllGlobals(), which does not touch defineProperty mutations
- Await the dropped invokeMenuCallback promise in
  litegraphService.core.test.ts so the assertion cannot race the
  callback
- Remove module-load global.fetch/global.URL mocks in
  mediaCacheService.test.ts that pre-empted and poisoned the
  per-test vi.stubGlobal teardown
- Add coverage for the null-link guards in LGraph.configure() and
  compressWidgetInputSlots(), and for PartnerNodesList rendering
  badges from app.rootGraph
- Restore the no-op updateModified() test and widen isobmff routing
  coverage to all six mime/extension branches
- Prefer fromPartial over raw casts in domWidget.test.ts and
  widgets.test.ts
2026-07-07 21:16:10 -07:00
huang47
994ec7ba6f test: replace change-detector default assertions with behavioral checks 2026-07-06 15:55:26 -07:00
13 changed files with 191 additions and 65 deletions

View File

@@ -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')

View 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()
})
})

View File

@@ -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'

View File

@@ -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()

View File

@@ -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)
})
})

View File

@@ -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', () => {

View File

@@ -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({

View File

@@ -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 () => {

View File

@@ -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'] = []) {

View File

@@ -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()

View File

@@ -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'

View File

@@ -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', () => {

View File

@@ -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: [