Merge branch 'main' into webcam-capture

This commit is contained in:
Johnpaul Chiwetelu
2026-01-26 21:28:09 +01:00
committed by GitHub
22 changed files with 314 additions and 169 deletions

View File

@@ -85,11 +85,10 @@ describe('BypassButton', () => {
})
it('should show bypassed styling when node is bypassed', async () => {
const bypassedNode: Partial<LGraphNode> = {
...getMockLGraphNode(),
const bypassedNode = Object.assign(getMockLGraphNode(), {
mode: LGraphEventMode.BYPASS
}
canvasStore.selectedItems = [bypassedNode as LGraphNode]
})
canvasStore.selectedItems = [bypassedNode]
vi.spyOn(commandStore, 'execute').mockResolvedValue()
const wrapper = mountComponent()

View File

@@ -14,11 +14,13 @@ import { useGraphHierarchy } from './useGraphHierarchy'
vi.mock('@/renderer/core/canvas/canvasStore')
function createMockNode(overrides: Partial<LGraphNode> = {}): LGraphNode {
return {
...createMockLGraphNode(),
boundingRect: new Rectangle(100, 100, 50, 50),
...overrides
} as LGraphNode
return Object.assign(
createMockLGraphNode(),
{
boundingRect: new Rectangle(100, 100, 50, 50)
},
overrides
)
}
function createMockGroup(overrides: Partial<LGraphGroup> = {}): LGraphGroup {

View File

@@ -46,8 +46,8 @@ function applyToGraph(this: LGraphNode, extraLinks: LLink[] = []) {
}
}
function onNodeCreated(this: LGraphNode) {
this.applyToGraph = useChainCallback(this.applyToGraph, applyToGraph)
function onCustomComboCreated(this: LGraphNode) {
this.applyToGraph = applyToGraph
const comboWidget = this.widgets![0]
const values = shallowReactive<string[]>([])
@@ -114,13 +114,97 @@ function onNodeCreated(this: LGraphNode) {
addOption(this)
}
function onCustomIntCreated(this: LGraphNode) {
const valueWidget = this.widgets?.[0]
if (!valueWidget) return
Object.defineProperty(valueWidget.options, 'min', {
get: () => this.properties.min ?? -(2 ** 63),
set: (v) => {
this.properties.min = v
valueWidget.callback?.(valueWidget.value)
}
})
Object.defineProperty(valueWidget.options, 'max', {
get: () => this.properties.max ?? 2 ** 63,
set: (v) => {
this.properties.max = v
valueWidget.callback?.(valueWidget.value)
}
})
Object.defineProperty(valueWidget.options, 'step2', {
get: () => this.properties.step ?? 1,
set: (v) => {
this.properties.step = v
valueWidget.callback?.(valueWidget.value) // for vue reactivity
}
})
}
function onCustomFloatCreated(this: LGraphNode) {
const valueWidget = this.widgets?.[0]
if (!valueWidget) return
Object.defineProperty(valueWidget.options, 'min', {
get: () => this.properties.min ?? -Infinity,
set: (v) => {
this.properties.min = v
valueWidget.callback?.(valueWidget.value)
}
})
Object.defineProperty(valueWidget.options, 'max', {
get: () => this.properties.max ?? Infinity,
set: (v) => {
this.properties.max = v
valueWidget.callback?.(valueWidget.value)
}
})
Object.defineProperty(valueWidget.options, 'precision', {
get: () => this.properties.precision ?? 1,
set: (v) => {
this.properties.precision = v
valueWidget.callback?.(valueWidget.value)
}
})
Object.defineProperty(valueWidget.options, 'step2', {
get: () => {
if (this.properties.step) return this.properties.step
const { precision } = this.properties
return typeof precision === 'number' ? 5 * 10 ** -precision : 1
},
set: (v) => (this.properties.step = v)
})
Object.defineProperty(valueWidget.options, 'round', {
get: () => {
if (this.properties.round) return this.properties.round
const { precision } = this.properties
return typeof precision === 'number' ? 10 ** -precision : 0.1
},
set: (v) => {
this.properties.round = v
valueWidget.callback?.(valueWidget.value)
}
})
}
app.registerExtension({
name: 'Comfy.CustomCombo',
name: 'Comfy.CustomWidgets',
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== 'CustomCombo') return
nodeType.prototype.onNodeCreated = useChainCallback(
nodeType.prototype.onNodeCreated,
onNodeCreated
)
if (nodeData?.name === 'CustomCombo')
nodeType.prototype.onNodeCreated = useChainCallback(
nodeType.prototype.onNodeCreated,
onCustomComboCreated
)
else if (nodeData?.name === 'PrimitiveInt')
nodeType.prototype.onNodeCreated = useChainCallback(
nodeType.prototype.onNodeCreated,
onCustomIntCreated
)
else if (nodeData?.name === 'PrimitiveFloat')
nodeType.prototype.onNodeCreated = useChainCallback(
nodeType.prototype.onNodeCreated,
onCustomFloatCreated
)
}
})

View File

@@ -2,7 +2,7 @@ import { isCloud, isNightly } from '@/platform/distribution/types'
import './clipspace'
import './contextMenuFilter'
import './customCombo'
import './customWidgets'
import './dynamicPrompts'
import './editAttention'
import './electronAdapter'

View File

@@ -2603,6 +2603,10 @@ export class Subgraph
}
addInput(name: string, type: string): SubgraphInput {
if (name === null || type === null) {
throw new Error('Name and type are required for subgraph input')
}
this.events.dispatch('adding-input', { name, type })
const input = new SubgraphInput(
@@ -2621,6 +2625,10 @@ export class Subgraph
}
addOutput(name: string, type: string): SubgraphOutput {
if (name === null || type === null) {
throw new Error('Name and type are required for subgraph output')
}
this.events.dispatch('adding-output', { name, type })
const output = new SubgraphOutput(

View File

@@ -254,7 +254,10 @@ type KeysOfType<T, Match> = Exclude<
>
/** The names of all (optional) methods and functions in T */
export type MethodNames<T> = KeysOfType<T, ((...args: any) => any) | undefined>
export type MethodNames<T> = KeysOfType<
T,
((...args: unknown[]) => unknown) | undefined
>
export interface NewNodePosition {
node: LGraphNode
newPos: {
@@ -459,28 +462,6 @@ export interface ISubgraphInput extends INodeInputSlot {
_subgraphSlot: SubgraphInput
}
/**
* Shorthand for {@link Parameters} of optional callbacks.
* @example
* ```ts
* const { onClick } = CustomClass.prototype
* CustomClass.prototype.onClick = function (...args: CallbackParams<typeof onClick>) {
* const r = onClick?.apply(this, args)
* // ...
* return r
* }
* ```
*/
export type CallbackParams<T extends ((...args: any) => any) | undefined> =
Parameters<Exclude<T, undefined>>
/**
* Shorthand for {@link ReturnType} of optional callbacks.
* @see {@link CallbackParams}
*/
export type CallbackReturn<T extends ((...args: any) => any) | undefined> =
ReturnType<Exclude<T, undefined>>
/**
* An object that can be hovered over.
*/

View File

@@ -4,11 +4,7 @@ import { InvalidLinkError } from '@/lib/litegraph/src/infrastructure/InvalidLink
import { NullGraphError } from '@/lib/litegraph/src/infrastructure/NullGraphError'
import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError'
import { SlotIndexError } from '@/lib/litegraph/src/infrastructure/SlotIndexError'
import type {
CallbackParams,
CallbackReturn,
ISlotType
} from '@/lib/litegraph/src/interfaces'
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
import { LGraphEventMode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { Subgraph } from './Subgraph'
@@ -45,8 +41,8 @@ type ResolvedInput = {
*/
export class ExecutableNodeDTO implements ExecutableLGraphNode {
applyToGraph?(
...args: CallbackParams<typeof this.node.applyToGraph>
): CallbackReturn<typeof this.node.applyToGraph>
...args: Parameters<NonNullable<typeof this.node.applyToGraph>>
): ReturnType<NonNullable<typeof this.node.applyToGraph>>
/** The graph that this node is a part of. */
readonly graph: LGraph | Subgraph

View File

@@ -83,7 +83,9 @@ describe.skip('SubgraphEdgeCases - Invalid States', () => {
name: 'fake',
type: 'number',
disconnect: () => {}
} as any
} as Partial<Parameters<typeof subgraph.removeInput>[0]> as Parameters<
typeof subgraph.removeInput
>[0]
// Should throw appropriate error for non-existent input
expect(() => {
@@ -97,41 +99,43 @@ describe.skip('SubgraphEdgeCases - Invalid States', () => {
name: 'fake',
type: 'number',
disconnect: () => {}
} as any
} as Partial<Parameters<typeof subgraph.removeOutput>[0]> as Parameters<
typeof subgraph.removeOutput
>[0]
expect(() => {
subgraph.removeOutput(fakeOutput)
}).toThrow(/Output not found/) // Expected error
})
it('should handle null/undefined input names', () => {
it('should throw error for null/undefined input names', () => {
const subgraph = createTestSubgraph()
// ISSUE: Current implementation allows null/undefined names which may cause runtime errors
// TODO: Consider adding validation to prevent null/undefined names
// This test documents the current permissive behavior
expect(() => {
subgraph.addInput(null as any, 'number')
}).not.toThrow() // Current behavior: allows null
const nullString: string = null!
const undefinedString: string = undefined!
expect(() => {
subgraph.addInput(undefined as any, 'number')
}).not.toThrow() // Current behavior: allows undefined
subgraph.addInput(nullString, 'number')
}).toThrow()
expect(() => {
subgraph.addInput(undefinedString, 'number')
}).toThrow()
})
it('should handle null/undefined output names', () => {
const subgraph = createTestSubgraph()
// ISSUE: Current implementation allows null/undefined names which may cause runtime errors
// TODO: Consider adding validation to prevent null/undefined names
// This test documents the current permissive behavior
expect(() => {
subgraph.addOutput(null as any, 'number')
}).not.toThrow() // Current behavior: allows null
const nullString: string = null!
const undefinedString: string = undefined!
expect(() => {
subgraph.addOutput(undefined as any, 'number')
}).not.toThrow() // Current behavior: allows undefined
subgraph.addOutput(nullString, 'number')
}).toThrow()
expect(() => {
subgraph.addOutput(undefinedString, 'number')
}).toThrow()
})
it('should handle empty string names', () => {
@@ -151,14 +155,16 @@ describe.skip('SubgraphEdgeCases - Invalid States', () => {
it('should handle undefined types gracefully', () => {
const subgraph = createTestSubgraph()
// Undefined type should not crash but may have default behavior
const undefinedString: string = undefined!
// Undefined type should throw error
expect(() => {
subgraph.addInput('test', undefined as any)
}).not.toThrow()
subgraph.addInput('test', undefinedString)
}).toThrow()
expect(() => {
subgraph.addOutput('test', undefined as any)
}).not.toThrow()
subgraph.addOutput('test', undefinedString)
}).toThrow()
})
it('should handle duplicate slot names', () => {

View File

@@ -10,6 +10,12 @@ import {
createTestSubgraphNode
} from './__fixtures__/subgraphHelpers'
type InputWithWidget = {
_widget?: IWidget | { type: string; value: unknown; name: string }
_connection?: { id: number; type: string }
_listenerController?: AbortController
}
describe.skip('SubgraphNode Memory Management', () => {
describe.skip('Event Listener Cleanup', () => {
it('should register event listeners on construction', () => {
@@ -308,14 +314,14 @@ describe.skip('SubgraphMemory - Widget Reference Management', () => {
// Set widget reference
if (input && '_widget' in input) {
;(input as any)._widget = mockWidget
expect((input as any)._widget).toBe(mockWidget)
;(input as InputWithWidget)._widget = mockWidget
expect((input as InputWithWidget)._widget).toBe(mockWidget)
}
// Clear widget reference
if (input && '_widget' in input) {
;(input as any)._widget = undefined
expect((input as any)._widget).toBeUndefined()
;(input as InputWithWidget)._widget = undefined
expect((input as InputWithWidget)._widget).toBeUndefined()
}
}
)
@@ -360,30 +366,34 @@ describe.skip('SubgraphMemory - Widget Reference Management', () => {
// Set up references that should be cleaned up
const mockReferences = {
widget: { type: 'number', value: 42 },
widget: { type: 'number', value: 42, name: 'mock_widget' },
connection: { id: 1, type: 'number' },
listener: vi.fn()
}
// Set references
if (input) {
;(input as any)._widget = mockReferences.widget
;(input as any)._connection = mockReferences.connection
;(input as InputWithWidget)._widget = mockReferences.widget
;(input as InputWithWidget)._connection = mockReferences.connection
}
if (output) {
;(input as any)._connection = mockReferences.connection
;(input as InputWithWidget)._connection = mockReferences.connection
}
// Verify references are set
expect((input as any)?._widget).toBe(mockReferences.widget)
expect((input as any)?._connection).toBe(mockReferences.connection)
expect((input as InputWithWidget)?._widget).toBe(mockReferences.widget)
expect((input as InputWithWidget)?._connection).toBe(
mockReferences.connection
)
// Simulate proper cleanup (what onRemoved should do)
subgraphNode.onRemoved()
// Input-specific listeners should be cleaned up (this works)
if (input && '_listenerController' in input) {
expect((input as any)._listenerController?.signal.aborted).toBe(true)
expect(
(input as InputWithWidget)._listenerController?.signal.aborted
).toBe(true)
}
}
)

View File

@@ -9,6 +9,7 @@ import { describe, expect, it, vi } from 'vitest'
import type { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
import { subgraphTest } from './__fixtures__/subgraphFixtures'
import {
@@ -531,7 +532,7 @@ describe.skip('SubgraphNode Cleanup', () => {
// Now trigger an event - only node1 should respond
subgraph.events.dispatch('input-added', {
input: { name: 'test', type: 'number', id: 'test-id' } as any
input: { name: 'test', type: 'number', id: 'test-id' } as SubgraphInput
})
// Only node1 should have added an input
@@ -558,7 +559,7 @@ describe.skip('SubgraphNode Cleanup', () => {
// Trigger an event - no nodes should respond
subgraph.events.dispatch('input-added', {
input: { name: 'test', type: 'number', id: 'test-id' } as any
input: { name: 'test', type: 'number', id: 'test-id' } as SubgraphInput
})
// Without cleanup: all 3 removed nodes would have added an input

View File

@@ -3,12 +3,18 @@ import { describe, expect, it, vi } from 'vitest'
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import {
createTestSubgraph,
createTestSubgraphNode
} from './__fixtures__/subgraphHelpers'
interface MockPointerEvent {
canvasX: number
canvasY: number
}
describe.skip('SubgraphNode Title Button', () => {
describe.skip('Constructor', () => {
it('should automatically add enter_subgraph button', () => {
@@ -58,7 +64,7 @@ describe.skip('SubgraphNode Title Button', () => {
const canvas = {
openSubgraph: vi.fn(),
dispatch: vi.fn()
} as unknown as LGraphCanvas
} as Partial<LGraphCanvas> as LGraphCanvas
subgraphNode.onTitleButtonClick(enterButton, canvas)
@@ -78,7 +84,7 @@ describe.skip('SubgraphNode Title Button', () => {
const canvas = {
openSubgraph: vi.fn(),
dispatch: vi.fn()
} as unknown as LGraphCanvas
} as Partial<LGraphCanvas> as LGraphCanvas
subgraphNode.onTitleButtonClick(customButton, canvas)
@@ -119,16 +125,16 @@ describe.skip('SubgraphNode Title Button', () => {
const canvas = {
ctx: {
measureText: vi.fn().mockReturnValue({ width: 25 })
} as unknown as CanvasRenderingContext2D,
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D,
openSubgraph: vi.fn(),
dispatch: vi.fn()
} as unknown as LGraphCanvas
} as Partial<LGraphCanvas> as LGraphCanvas
// Simulate click on the enter button
const event = {
const event: MockPointerEvent = {
canvasX: 275, // Near right edge where button should be
canvasY: 80 // In title area
} as any
}
// Calculate node-relative position
const clickPosRelativeToNode: [number, number] = [
@@ -138,7 +144,7 @@ describe.skip('SubgraphNode Title Button', () => {
// @ts-expect-error onMouseDown possibly undefined
const handled = subgraphNode.onMouseDown(
event,
event as Partial<CanvasPointerEvent> as CanvasPointerEvent,
clickPosRelativeToNode,
canvas
)
@@ -156,16 +162,16 @@ describe.skip('SubgraphNode Title Button', () => {
const canvas = {
ctx: {
measureText: vi.fn().mockReturnValue({ width: 25 })
} as unknown as CanvasRenderingContext2D,
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D,
openSubgraph: vi.fn(),
dispatch: vi.fn()
} as unknown as LGraphCanvas
} as Partial<LGraphCanvas> as LGraphCanvas
// Click in the body of the node, not on button
const event = {
const event: MockPointerEvent = {
canvasX: 200, // Middle of node
canvasY: 150 // Body area
} as any
}
// Calculate node-relative position
const clickPosRelativeToNode: [number, number] = [
@@ -173,9 +179,8 @@ describe.skip('SubgraphNode Title Button', () => {
150 - subgraphNode.pos[1] // 150 - 100 = 50
]
// @ts-expect-error onMouseDown possibly undefined
const handled = subgraphNode.onMouseDown(
event,
const handled = subgraphNode.onMouseDown!(
event as Partial<CanvasPointerEvent> as CanvasPointerEvent,
clickPosRelativeToNode,
canvas
)
@@ -204,25 +209,24 @@ describe.skip('SubgraphNode Title Button', () => {
const canvas = {
ctx: {
measureText: vi.fn().mockReturnValue({ width: 25 })
} as unknown as CanvasRenderingContext2D,
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D,
openSubgraph: vi.fn(),
dispatch: vi.fn()
} as unknown as LGraphCanvas
} as Partial<LGraphCanvas> as LGraphCanvas
// Try to click on where the button would be
const event = {
const event: MockPointerEvent = {
canvasX: 275,
canvasY: 80
} as any
}
const clickPosRelativeToNode: [number, number] = [
275 - subgraphNode.pos[0], // 175
80 - subgraphNode.pos[1] // -20
]
// @ts-expect-error onMouseDown possibly undefined
const handled = subgraphNode.onMouseDown(
event,
const handled = subgraphNode.onMouseDown!(
event as Partial<CanvasPointerEvent> as CanvasPointerEvent,
clickPosRelativeToNode,
canvas
)

View File

@@ -1,13 +1,21 @@
// TODO: Fix these tests after migration
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { DefaultConnectionColors } from '@/lib/litegraph/src/interfaces'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { createTestSubgraph } from './__fixtures__/subgraphHelpers'
interface MockColorContext {
defaultInputColor: string
defaultOutputColor: string
getConnectedColor: ReturnType<typeof vi.fn>
getDisconnectedColor: ReturnType<typeof vi.fn>
}
describe.skip('SubgraphSlot visual feedback', () => {
let mockCtx: CanvasRenderingContext2D
let mockColorContext: any
let mockColorContext: MockColorContext
let globalAlphaValues: number[]
beforeEach(() => {
@@ -34,7 +42,8 @@ describe.skip('SubgraphSlot visual feedback', () => {
rect: vi.fn(),
fillText: vi.fn()
}
mockCtx = mockContext as unknown as CanvasRenderingContext2D
mockCtx =
mockContext as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
// Create a mock color context
mockColorContext = {
@@ -42,7 +51,7 @@ describe.skip('SubgraphSlot visual feedback', () => {
defaultOutputColor: '#00FF00',
getConnectedColor: vi.fn().mockReturnValue('#0000FF'),
getDisconnectedColor: vi.fn().mockReturnValue('#AAAAAA')
}
} as Partial<MockColorContext> as MockColorContext
})
it('should render SubgraphInput slots with full opacity when dragging from compatible slot', () => {
@@ -60,7 +69,8 @@ describe.skip('SubgraphSlot visual feedback', () => {
// Draw the slot with a compatible fromSlot
subgraphInput.draw({
ctx: mockCtx,
colorContext: mockColorContext,
colorContext:
mockColorContext as Partial<DefaultConnectionColors> as DefaultConnectionColors,
fromSlot: nodeInput,
editorAlpha: 1
})
@@ -80,7 +90,8 @@ describe.skip('SubgraphSlot visual feedback', () => {
// Draw subgraphInput2 while dragging from subgraphInput1 (incompatible - both are outputs inside subgraph)
subgraphInput2.draw({
ctx: mockCtx,
colorContext: mockColorContext,
colorContext:
mockColorContext as Partial<DefaultConnectionColors> as DefaultConnectionColors,
fromSlot: subgraphInput1,
editorAlpha: 1
})
@@ -105,7 +116,8 @@ describe.skip('SubgraphSlot visual feedback', () => {
// Draw the slot with a compatible fromSlot
subgraphOutput.draw({
ctx: mockCtx,
colorContext: mockColorContext,
colorContext:
mockColorContext as Partial<DefaultConnectionColors> as DefaultConnectionColors,
fromSlot: nodeOutput,
editorAlpha: 1
})
@@ -125,7 +137,8 @@ describe.skip('SubgraphSlot visual feedback', () => {
// Draw subgraphOutput2 while dragging from subgraphOutput1 (incompatible - both are inputs inside subgraph)
subgraphOutput2.draw({
ctx: mockCtx,
colorContext: mockColorContext,
colorContext:
mockColorContext as Partial<DefaultConnectionColors> as DefaultConnectionColors,
fromSlot: subgraphOutput1,
editorAlpha: 1
})
@@ -170,7 +183,8 @@ describe.skip('SubgraphSlot visual feedback', () => {
// Draw the SubgraphOutput slot while dragging from a node output with incompatible type
subgraphOutput.draw({
ctx: mockCtx,
colorContext: mockColorContext,
colorContext:
mockColorContext as Partial<DefaultConnectionColors> as DefaultConnectionColors,
fromSlot: nodeStringOutput,
editorAlpha: 1
})

View File

@@ -18,7 +18,7 @@ import {
function createNodeWithWidget(
title: string,
widgetType: TWidgetType = 'number',
widgetValue: any = 42,
widgetValue: unknown = 42,
slotType: ISlotType = 'number',
tooltip?: string
) {

View File

@@ -1,3 +1,4 @@
import type { Mock } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { truncateText } from '@/lib/litegraph/src/litegraph'
@@ -5,8 +6,13 @@ import { truncateText } from '@/lib/litegraph/src/litegraph'
describe('truncateText', () => {
const createMockContext = (charWidth: number = 10) => {
return {
measureText: vi.fn((text: string) => ({ width: text.length * charWidth }))
} as unknown as CanvasRenderingContext2D
measureText: vi.fn(
(text: string) =>
({
width: text.length * charWidth
}) as TextMetrics
)
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
}
it('should return original text if it fits within maxWidth', () => {
@@ -57,7 +63,7 @@ describe('truncateText', () => {
// Verify binary search efficiency - should not measure every possible substring
// Binary search for 100 chars should take around log2(100) ≈ 7 iterations
// Plus a few extra calls for measuring the full text and ellipsis
const callCount = (ctx.measureText as any).mock.calls.length
const callCount = (ctx.measureText as Mock).mock.calls.length
expect(callCount).toBeLessThan(20)
expect(callCount).toBeGreaterThan(5)
})

View File

@@ -389,8 +389,9 @@ describe('ComboWidget', () => {
node.size = [200, 30]
const mockContextMenu = vi.fn()
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -426,8 +427,9 @@ describe('ComboWidget', () => {
node.size = [200, 30]
const mockContextMenu = vi.fn()
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -463,8 +465,9 @@ describe('ComboWidget', () => {
.mockImplementation(function (_values, options) {
capturedCallback = options.callback
})
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
const setValueSpy = vi.spyOn(widget, 'setValue')
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -506,8 +509,9 @@ describe('ComboWidget', () => {
.mockImplementation(function (_values, options) {
capturedCallback = options.callback
})
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
const setValueSpy = vi.spyOn(widget, 'setValue')
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -538,8 +542,9 @@ describe('ComboWidget', () => {
node.size = [200, 30]
const mockContextMenu = vi.fn()
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -572,8 +577,9 @@ describe('ComboWidget', () => {
node.size = [200, 30]
const mockContextMenu = vi.fn()
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -768,8 +774,9 @@ describe('ComboWidget', () => {
.mockImplementation(function () {
this.addItem = mockAddItem
})
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
// Should show formatted labels in dropdown
@@ -831,8 +838,9 @@ describe('ComboWidget', () => {
capturedCallback = options.callback
this.addItem = mockAddItem
})
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
const setValueSpy = vi.spyOn(widget, 'setValue')
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -885,8 +893,9 @@ describe('ComboWidget', () => {
capturedCallback = options.callback
this.addItem = mockAddItem
})
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -964,8 +973,9 @@ describe('ComboWidget', () => {
.mockImplementation(function () {
this.addItem = mockAddItem
})
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -1012,8 +1022,9 @@ describe('ComboWidget', () => {
node.size = [200, 30]
const mockContextMenu = vi.fn<typeof LiteGraph.ContextMenu>()
LiteGraph.ContextMenu =
mockContextMenu as unknown as typeof LiteGraph.ContextMenu
LiteGraph.ContextMenu = mockContextMenu as Partial<
typeof LiteGraph.ContextMenu
> as typeof LiteGraph.ContextMenu
widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
@@ -1051,7 +1062,8 @@ describe('ComboWidget', () => {
createMockWidgetConfig({
name: 'mode',
value: 'test',
options: { values: null as any }
// @ts-expect-error - Testing with intentionally invalid null value
options: { values: null }
}),
node
)

View File

@@ -67,7 +67,7 @@ const MOCK_ASSETS = {
} as const
// Helper functions
function mockApiResponse(assets: any[], options = {}) {
function mockApiResponse(assets: unknown[], options = {}) {
const response = {
assets,
total: assets.length,

View File

@@ -8,6 +8,17 @@ import {
getSettingInfo,
useSettingStore
} from '@/platform/settings/settingStore'
import type { SettingTreeNode } from '@/platform/settings/settingStore'
// Test-specific type for mock settings
interface MockSettingParams {
id: string
name: string
type: string
defaultValue: unknown
category?: string[]
deprecated?: boolean
}
// Mock dependencies
vi.mock('@/i18n', () => ({
@@ -20,8 +31,8 @@ vi.mock('@/platform/settings/settingStore', () => ({
}))
describe('useSettingSearch', () => {
let mockSettingStore: any
let mockSettings: any
let mockSettingStore: ReturnType<typeof useSettingStore>
let mockSettings: Record<string, MockSettingParams>
beforeEach(() => {
setActivePinia(createPinia())
@@ -70,11 +81,11 @@ describe('useSettingSearch', () => {
// Mock setting store
mockSettingStore = {
settingsById: mockSettings
}
} as ReturnType<typeof useSettingStore>
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
// Mock getSettingInfo function
vi.mocked(getSettingInfo).mockImplementation((setting: any) => {
vi.mocked(getSettingInfo).mockImplementation((setting) => {
const parts = setting.category || setting.id.split('.')
return {
category: parts[0] ?? 'Other',
@@ -301,8 +312,8 @@ describe('useSettingSearch', () => {
const search = useSettingSearch()
search.filteredSettingIds.value = ['Category.Setting1', 'Other.Setting3']
const activeCategory = { label: 'Category' } as any
const results = search.getSearchResults(activeCategory)
const activeCategory: Partial<SettingTreeNode> = { label: 'Category' }
const results = search.getSearchResults(activeCategory as SettingTreeNode)
expect(results).toEqual([
{

View File

@@ -6,6 +6,7 @@ import {
useSettingStore
} from '@/platform/settings/settingStore'
import type { SettingParams } from '@/platform/settings/types'
import type { Settings } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
@@ -45,7 +46,9 @@ describe('useSettingStore', () => {
describe('loadSettingValues', () => {
it('should load settings from API', async () => {
const mockSettings = { 'test.setting': 'value' }
vi.mocked(api.getSettings).mockResolvedValue(mockSettings as any)
vi.mocked(api.getSettings).mockResolvedValue(
mockSettings as Partial<Settings> as Settings
)
await store.loadSettingValues()

View File

@@ -37,6 +37,13 @@ export interface SurveyResponses {
making?: string[]
}
export interface SurveyResponsesNormalized extends SurveyResponses {
industry_normalized?: string
industry_raw?: string
useCase_normalized?: string
useCase_raw?: string
}
/**
* Run button tracking properties
*/

View File

@@ -328,9 +328,9 @@ describe('normalizeIndustry', () => {
})
it('should handle null and invalid inputs', () => {
expect(normalizeIndustry(null as any)).toBe('Other / Undefined')
expect(normalizeIndustry(undefined as any)).toBe('Other / Undefined')
expect(normalizeIndustry(123 as any)).toBe('Other / Undefined')
expect(normalizeIndustry(null)).toBe('Other / Undefined')
expect(normalizeIndustry(undefined)).toBe('Other / Undefined')
expect(normalizeIndustry(123)).toBe('Other / Undefined')
})
})
@@ -508,7 +508,7 @@ describe('normalizeUseCase', () => {
expect(normalizeUseCase('none')).toBe('Other / Undefined')
expect(normalizeUseCase('undefined')).toBe('Other / Undefined')
expect(normalizeUseCase('')).toBe('Other / Undefined')
expect(normalizeUseCase(null as any)).toBe('Other / Undefined')
expect(normalizeUseCase(null)).toBe('Other / Undefined')
})
})

View File

@@ -6,6 +6,7 @@
* Uses Fuse.js for fuzzy matching against category keywords.
*/
import Fuse from 'fuse.js'
import type { SurveyResponses, SurveyResponsesNormalized } from '../types'
interface CategoryMapping {
name: string
@@ -583,21 +584,19 @@ export function normalizeUseCase(rawUseCase: unknown): string {
* Apply normalization to survey responses
* Creates both normalized and raw versions of responses
*/
export function normalizeSurveyResponses(responses: {
industry?: string
useCase?: string
[key: string]: any
}) {
const normalized = { ...responses }
export function normalizeSurveyResponses(
responses: SurveyResponses
): SurveyResponsesNormalized {
const normalized: SurveyResponsesNormalized = { ...responses }
// Normalize industry
if (responses.industry) {
if (typeof responses.industry === 'string') {
normalized.industry_normalized = normalizeIndustry(responses.industry)
normalized.industry_raw = responses.industry
}
// Normalize use case
if (responses.useCase) {
if (typeof responses.useCase === 'string') {
normalized.useCase_normalized = normalizeUseCase(responses.useCase)
normalized.useCase_raw = responses.useCase
}

View File

@@ -191,13 +191,15 @@ export function createMockLGraphNodeWithArrayBoundingRect(
* Creates a mock FileList from an array of files
*/
export function createMockFileList(files: File[]): FileList {
const fileList = {
...files,
length: files.length,
item: (index: number) => files[index] ?? null,
[Symbol.iterator]: function* () {
yield* files
}
}
const fileList = Object.assign(
{
length: files.length,
item: (index: number) => files[index] ?? null,
[Symbol.iterator]: function* () {
yield* files
}
},
files
)
return fileList as FileList
}