mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 17:37:46 +00:00
refactor: eliminate unsafe type assertions from Group 2 test files (#8258)
## Summary Improved type safety in test files by eliminating unsafe type assertions and adopting official testing patterns. Reduced unsafe `as unknown as` type assertions and eliminated all `null!` assertions. ## Changes - **Adopted @pinia/testing patterns** - Replaced manual Pinia store mocking with `createTestingPinia()` in `useSelectionState.test.ts` - Eliminated ~120 lines of mock boilerplate - Created `createMockSettingStore()` helper to replace duplicated store mocks in `useCoreCommands.test.ts` - **Eliminated unsafe null assertions** - Created explicit `MockMaskEditorStore` interface with proper nullable types in `useCanvasTools.test.ts` - Replaced `null!` initializations with `null` and used `!` at point of use or `?.` for optional chaining - **Made partial mock intent explicit** - Updated test utilities in `litegraphTestUtils.ts` to use explicit `Partial<T>` typing - Changed cast pattern from `as T` to `as Partial<T> as T` to show incomplete mock intent - Applied to `createMockLGraphNode()`, `createMockPositionable()`, and `createMockLGraphGroup()` - **Created centralized mock utilities** in `src/utils/__tests__/litegraphTestUtils.ts` - `createMockLGraphNode()`, `createMockPositionable()`, `createMockLGraphGroup()`, `createMockSubgraphNode()` - Updated 8+ test files to use centralized utilities - Used union types `Partial<T> | Record<string, unknown>` for flexible mock creation ## Results - ✅ 0 typecheck errors - ✅ 0 lint errors - ✅ All tests passing in modified files - ✅ Eliminated all `null!` assertions - ✅ Reduced unsafe double-cast patterns significantly ## Files Modified (18) - `src/components/graph/SelectionToolbox.test.ts` - `src/components/graph/selectionToolbox/{BypassButton,ColorPickerButton,ExecuteButton}.test.ts` - `src/components/sidebar/tabs/queue/ResultGallery.test.ts` - `src/composables/canvas/useSelectedLiteGraphItems.test.ts` - `src/composables/graph/{useGraphHierarchy,useSelectionState}.test.ts` - `src/composables/maskeditor/{useCanvasHistory,useCanvasManager,useCanvasTools,useCanvasTransform}.test.ts` - `src/composables/node/{useNodePricing,useWatchWidget}.test.ts` - `src/composables/{useBrowserTabTitle,useCoreCommands}.test.ts` - `src/utils/__tests__/litegraphTestUtils.ts` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8258-refactor-eliminate-unsafe-type-assertions-from-Group-2-test-files-2f16d73d365081549c65fd546cc7c765) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
This commit is contained in:
committed by
GitHub
parent
6b6b467e68
commit
b1d8bf0b13
@@ -4,6 +4,7 @@ import { CREDITS_PER_USD, formatCredits } from '@/base/credits/comfyCredits'
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { PriceBadge } from '@/schemas/nodeDefSchema'
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test Types
|
||||
@@ -26,13 +27,6 @@ interface MockNodeData {
|
||||
price_badge?: PriceBadge
|
||||
}
|
||||
|
||||
interface MockNode {
|
||||
id: string
|
||||
widgets: MockNodeWidget[]
|
||||
inputs: MockNodeInput[]
|
||||
constructor: { nodeData: MockNodeData }
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test Helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -80,8 +74,8 @@ function createMockNodeWithPriceBadge(
|
||||
link: connected ? 1 : null
|
||||
}))
|
||||
|
||||
const node: MockNode = {
|
||||
id: Math.random().toString(),
|
||||
const baseNode = createMockLGraphNode()
|
||||
return Object.assign(baseNode, {
|
||||
widgets: mockWidgets,
|
||||
inputs: mockInputs,
|
||||
constructor: {
|
||||
@@ -91,9 +85,7 @@ function createMockNodeWithPriceBadge(
|
||||
price_badge: priceBadge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node as unknown as LGraphNode
|
||||
})
|
||||
}
|
||||
|
||||
/** Helper to create a price badge with defaults */
|
||||
@@ -108,6 +100,20 @@ const priceBadge = (
|
||||
depends_on: { widgets, inputs, input_groups: inputGroups }
|
||||
})
|
||||
|
||||
/** Helper to create a mock node for edge case testing */
|
||||
function createMockNode(
|
||||
nodeData: MockNodeData,
|
||||
widgets: MockNodeWidget[] = [],
|
||||
inputs: MockNodeInput[] = []
|
||||
): LGraphNode {
|
||||
const baseNode = createMockLGraphNode()
|
||||
return Object.assign(baseNode, {
|
||||
widgets,
|
||||
inputs,
|
||||
constructor: { nodeData }
|
||||
})
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -456,37 +462,23 @@ describe('useNodePricing', () => {
|
||||
describe('edge cases', () => {
|
||||
it('should return empty string for non-API nodes', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node: MockNode = {
|
||||
id: 'test',
|
||||
widgets: [],
|
||||
inputs: [],
|
||||
constructor: {
|
||||
nodeData: {
|
||||
name: 'RegularNode',
|
||||
api_node: false
|
||||
}
|
||||
}
|
||||
}
|
||||
const node = createMockNode({
|
||||
name: 'RegularNode',
|
||||
api_node: false
|
||||
})
|
||||
|
||||
const price = getNodeDisplayPrice(node as unknown as LGraphNode)
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('')
|
||||
})
|
||||
|
||||
it('should return empty string for nodes without price_badge', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node: MockNode = {
|
||||
id: 'test',
|
||||
widgets: [],
|
||||
inputs: [],
|
||||
constructor: {
|
||||
nodeData: {
|
||||
name: 'ApiNodeNoPricing',
|
||||
api_node: true
|
||||
}
|
||||
}
|
||||
}
|
||||
const node = createMockNode({
|
||||
name: 'ApiNodeNoPricing',
|
||||
api_node: true
|
||||
})
|
||||
|
||||
const price = getNodeDisplayPrice(node as unknown as LGraphNode)
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('')
|
||||
})
|
||||
|
||||
@@ -559,37 +551,23 @@ describe('useNodePricing', () => {
|
||||
|
||||
it('should return undefined for nodes without price_badge', () => {
|
||||
const { getNodePricingConfig } = useNodePricing()
|
||||
const node: MockNode = {
|
||||
id: 'test',
|
||||
widgets: [],
|
||||
inputs: [],
|
||||
constructor: {
|
||||
nodeData: {
|
||||
name: 'NoPricingNode',
|
||||
api_node: true
|
||||
}
|
||||
}
|
||||
}
|
||||
const node = createMockNode({
|
||||
name: 'NoPricingNode',
|
||||
api_node: true
|
||||
})
|
||||
|
||||
const config = getNodePricingConfig(node as unknown as LGraphNode)
|
||||
const config = getNodePricingConfig(node)
|
||||
expect(config).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for non-API nodes', () => {
|
||||
const { getNodePricingConfig } = useNodePricing()
|
||||
const node: MockNode = {
|
||||
id: 'test',
|
||||
widgets: [],
|
||||
inputs: [],
|
||||
constructor: {
|
||||
nodeData: {
|
||||
name: 'RegularNode',
|
||||
api_node: false
|
||||
}
|
||||
}
|
||||
}
|
||||
const node = createMockNode({
|
||||
name: 'RegularNode',
|
||||
api_node: false
|
||||
})
|
||||
|
||||
const config = getNodePricingConfig(node as unknown as LGraphNode)
|
||||
const config = getNodePricingConfig(node)
|
||||
expect(config).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -642,21 +620,12 @@ describe('useNodePricing', () => {
|
||||
|
||||
it('should not throw for non-API nodes', () => {
|
||||
const { triggerPriceRecalculation } = useNodePricing()
|
||||
const node: MockNode = {
|
||||
id: 'test',
|
||||
widgets: [],
|
||||
inputs: [],
|
||||
constructor: {
|
||||
nodeData: {
|
||||
name: 'RegularNode',
|
||||
api_node: false
|
||||
}
|
||||
}
|
||||
}
|
||||
const node = createMockNode({
|
||||
name: 'RegularNode',
|
||||
api_node: false
|
||||
})
|
||||
|
||||
expect(() =>
|
||||
triggerPriceRecalculation(node as unknown as LGraphNode)
|
||||
).not.toThrow()
|
||||
expect(() => triggerPriceRecalculation(node)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -751,35 +720,32 @@ describe('useNodePricing', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
|
||||
// Create a node with autogrow-style inputs (group.input1, group.input2, etc.)
|
||||
const node: MockNode = {
|
||||
id: Math.random().toString(),
|
||||
widgets: [],
|
||||
inputs: [
|
||||
const node = createMockNode(
|
||||
{
|
||||
name: 'TestInputGroupNode',
|
||||
api_node: true,
|
||||
price_badge: {
|
||||
engine: 'jsonata',
|
||||
expr: '{"type":"usd","usd": inputGroups.videos * 0.05}',
|
||||
depends_on: {
|
||||
widgets: [],
|
||||
inputs: [],
|
||||
input_groups: ['videos']
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
[
|
||||
{ name: 'videos.clip1', link: 1 }, // connected
|
||||
{ name: 'videos.clip2', link: 2 }, // connected
|
||||
{ name: 'videos.clip3', link: null }, // disconnected
|
||||
{ name: 'other_input', link: 3 } // connected but not in group
|
||||
],
|
||||
constructor: {
|
||||
nodeData: {
|
||||
name: 'TestInputGroupNode',
|
||||
api_node: true,
|
||||
price_badge: {
|
||||
engine: 'jsonata',
|
||||
expr: '{"type":"usd","usd": inputGroups.videos * 0.05}',
|
||||
depends_on: {
|
||||
widgets: [],
|
||||
inputs: [],
|
||||
input_groups: ['videos']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
getNodeDisplayPrice(node as unknown as LGraphNode)
|
||||
getNodeDisplayPrice(node)
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
const price = getNodeDisplayPrice(node as unknown as LGraphNode)
|
||||
const price = getNodeDisplayPrice(node)
|
||||
// 2 connected inputs in 'videos' group * 0.05 = 0.10
|
||||
expect(price).toBe(creditsLabel(0.1))
|
||||
})
|
||||
|
||||
@@ -3,11 +3,12 @@ import { nextTick } from 'vue'
|
||||
|
||||
import { useComputedWithWidgetWatch } from '@/composables/node/useWatchWidget'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
// Mock useChainCallback
|
||||
vi.mock('@/composables/functional/useChainCallback', () => ({
|
||||
useChainCallback: vi.fn((original, newCallback) => {
|
||||
return function (this: any, ...args: any[]) {
|
||||
return function (this: unknown, ...args: unknown[]) {
|
||||
original?.call(this, ...args)
|
||||
newCallback.call(this, ...args)
|
||||
}
|
||||
@@ -18,11 +19,12 @@ describe('useComputedWithWidgetWatch', () => {
|
||||
const createMockNode = (
|
||||
widgets: Array<{
|
||||
name: string
|
||||
value: any
|
||||
callback?: (...args: any[]) => void
|
||||
value: unknown
|
||||
callback?: (...args: unknown[]) => void
|
||||
}> = []
|
||||
) => {
|
||||
const mockNode = {
|
||||
): LGraphNode => {
|
||||
const baseNode = createMockLGraphNode()
|
||||
return Object.assign(baseNode, {
|
||||
widgets: widgets.map((widget) => ({
|
||||
name: widget.name,
|
||||
value: widget.value,
|
||||
@@ -31,9 +33,7 @@ describe('useComputedWithWidgetWatch', () => {
|
||||
graph: {
|
||||
setDirtyCanvas: vi.fn()
|
||||
}
|
||||
} as unknown as LGraphNode
|
||||
|
||||
return mockNode
|
||||
})
|
||||
}
|
||||
|
||||
it('should create a reactive computed that responds to widget changes', async () => {
|
||||
@@ -59,9 +59,9 @@ describe('useComputedWithWidgetWatch', () => {
|
||||
|
||||
// Change widget value and trigger callback
|
||||
const widthWidget = mockNode.widgets?.find((w) => w.name === 'width')
|
||||
if (widthWidget) {
|
||||
if (widthWidget && widthWidget.callback) {
|
||||
widthWidget.value = 150
|
||||
;(widthWidget.callback as any)?.()
|
||||
widthWidget.callback(widthWidget.value)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
@@ -89,9 +89,9 @@ describe('useComputedWithWidgetWatch', () => {
|
||||
|
||||
// Change observed widget
|
||||
const widthWidget = mockNode.widgets?.find((w) => w.name === 'width')
|
||||
if (widthWidget) {
|
||||
if (widthWidget && widthWidget.callback) {
|
||||
widthWidget.value = 150
|
||||
;(widthWidget.callback as any)?.()
|
||||
widthWidget.callback(widthWidget.value)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
@@ -117,9 +117,9 @@ describe('useComputedWithWidgetWatch', () => {
|
||||
|
||||
// Change widget value
|
||||
const widget = mockNode.widgets?.[0]
|
||||
if (widget) {
|
||||
if (widget && widget.callback) {
|
||||
widget.value = 20
|
||||
;(widget.callback as any)?.()
|
||||
widget.callback(widget.value)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
@@ -139,9 +139,9 @@ describe('useComputedWithWidgetWatch', () => {
|
||||
|
||||
// Change widget value
|
||||
const widget = mockNode.widgets?.[0]
|
||||
if (widget) {
|
||||
if (widget && widget.callback) {
|
||||
widget.value = 20
|
||||
;(widget.callback as any)?.()
|
||||
widget.callback(widget.value)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
@@ -171,8 +171,8 @@ describe('useComputedWithWidgetWatch', () => {
|
||||
|
||||
// Trigger widget callback
|
||||
const widget = mockNode.widgets?.[0]
|
||||
if (widget) {
|
||||
;(widget.callback as any)?.()
|
||||
if (widget && widget.callback) {
|
||||
widget.callback(widget.value)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
Reference in New Issue
Block a user