mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 08:30:08 +00:00
chore: migrate tests from tests-ui/ to colocate with source files (#7811)
## Summary Migrates all unit tests from `tests-ui/` to colocate with their source files in `src/`, improving discoverability and maintainability. ## Changes - **What**: Relocated all unit tests to be adjacent to the code they test, following the `<source>.test.ts` naming convention - **Config**: Updated `vitest.config.ts` to remove `tests-ui` include pattern and `@tests-ui` alias - **Docs**: Moved testing documentation to `docs/testing/` with updated paths and patterns ## Review Focus - Migration patterns documented in `temp/plans/migrate-tests-ui-to-src.md` - Tests use `@/` path aliases instead of relative imports - Shared fixtures placed in `__fixtures__/` directories ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7811-chore-migrate-tests-from-tests-ui-to-colocate-with-source-files-2da6d73d36508147a4cce85365dee614) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
248
src/utils/colorUtil.test.ts
Normal file
248
src/utils/colorUtil.test.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
adjustColor,
|
||||
hexToRgb,
|
||||
hsbToRgb,
|
||||
parseToRgb,
|
||||
rgbToHex
|
||||
} from '@/utils/colorUtil'
|
||||
|
||||
interface ColorTestCase {
|
||||
hex: string
|
||||
rgb: string
|
||||
rgba: string
|
||||
hsl: string
|
||||
hsla: string
|
||||
lightExpected: string
|
||||
transparentExpected: string
|
||||
lightTransparentExpected: string
|
||||
}
|
||||
|
||||
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
|
||||
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
memoize: (fn: any) => fn
|
||||
}))
|
||||
|
||||
const targetOpacity = 0.5
|
||||
const targetLightness = 0.5
|
||||
|
||||
const assertColorVariationsMatch = (variations: string[], adjustment: any) => {
|
||||
for (let i = 0; i < variations.length - 1; i++) {
|
||||
expect(adjustColor(variations[i], adjustment)).toBe(
|
||||
adjustColor(variations[i + 1], adjustment)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const colors: Record<string, ColorTestCase> = {
|
||||
green: {
|
||||
hex: '#073642',
|
||||
rgb: 'rgb(7, 54, 66)',
|
||||
rgba: 'rgba(7, 54, 66, 1)',
|
||||
hsl: 'hsl(192, 80.8%, 14.3%)',
|
||||
hsla: 'hsla(192, 80.8%, 14.3%, 1)',
|
||||
lightExpected: 'hsla(192, 80.8%, 64.3%, 1)',
|
||||
transparentExpected: 'hsla(192, 80.8%, 14.3%, 0.5)',
|
||||
lightTransparentExpected: 'hsla(192, 80.8%, 64.3%, 0.5)'
|
||||
},
|
||||
blue: {
|
||||
hex: '#00008B',
|
||||
rgb: 'rgb(0,0,139)',
|
||||
rgba: 'rgba(0,0,139,1)',
|
||||
hsl: 'hsl(240,100%,27.3%)',
|
||||
hsla: 'hsl(240,100%,27.3%,1)',
|
||||
lightExpected: 'hsla(240, 100%, 77.3%, 1)',
|
||||
transparentExpected: 'hsla(240, 100%, 27.3%, 0.5)',
|
||||
lightTransparentExpected: 'hsla(240, 100%, 77.3%, 0.5)'
|
||||
}
|
||||
}
|
||||
|
||||
const formats: ColorFormat[] = ['hex', 'rgb', 'rgba', 'hsl', 'hsla']
|
||||
|
||||
describe('colorUtil conversions', () => {
|
||||
describe('hexToRgb / rgbToHex', () => {
|
||||
it('converts 6-digit hex to RGB', () => {
|
||||
expect(hexToRgb('#ff0000')).toEqual({ r: 255, g: 0, b: 0 })
|
||||
expect(hexToRgb('#00ff00')).toEqual({ r: 0, g: 255, b: 0 })
|
||||
expect(hexToRgb('#0000ff')).toEqual({ r: 0, g: 0, b: 255 })
|
||||
})
|
||||
|
||||
it('converts 3-digit hex to RGB', () => {
|
||||
expect(hexToRgb('#f00')).toEqual({ r: 255, g: 0, b: 0 })
|
||||
expect(hexToRgb('#0f0')).toEqual({ r: 0, g: 255, b: 0 })
|
||||
expect(hexToRgb('#00f')).toEqual({ r: 0, g: 0, b: 255 })
|
||||
})
|
||||
|
||||
it('converts RGB to lowercase #hex and clamps values', () => {
|
||||
expect(rgbToHex({ r: 255, g: 0, b: 0 })).toBe('#ff0000')
|
||||
expect(rgbToHex({ r: 0, g: 255, b: 0 })).toBe('#00ff00')
|
||||
expect(rgbToHex({ r: 0, g: 0, b: 255 })).toBe('#0000ff')
|
||||
// out-of-range should clamp
|
||||
expect(rgbToHex({ r: -10, g: 300, b: 16 })).toBe('#00ff10')
|
||||
})
|
||||
|
||||
it('round-trips #hex -> rgb -> #hex', () => {
|
||||
const hex = '#123abc'
|
||||
expect(rgbToHex(hexToRgb(hex))).toBe('#123abc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseToRgb', () => {
|
||||
it('parses #hex', () => {
|
||||
expect(parseToRgb('#ff0000')).toEqual({ r: 255, g: 0, b: 0 })
|
||||
})
|
||||
|
||||
it('parses rgb()/rgba()', () => {
|
||||
expect(parseToRgb('rgb(255, 0, 0)')).toEqual({ r: 255, g: 0, b: 0 })
|
||||
expect(parseToRgb('rgba(255,0,0,0.5)')).toEqual({ r: 255, g: 0, b: 0 })
|
||||
})
|
||||
|
||||
it('parses hsl()/hsla()', () => {
|
||||
expect(parseToRgb('hsl(0, 100%, 50%)')).toEqual({ r: 255, g: 0, b: 0 })
|
||||
const green = parseToRgb('hsla(120, 100%, 50%, 0.7)')
|
||||
expect(green.r).toBe(0)
|
||||
expect(green.g).toBe(255)
|
||||
expect(green.b).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hsbToRgb', () => {
|
||||
it('converts HSB to primary RGB colors', () => {
|
||||
expect(hsbToRgb({ h: 0, s: 100, b: 100 })).toEqual({ r: 255, g: 0, b: 0 })
|
||||
expect(hsbToRgb({ h: 120, s: 100, b: 100 })).toEqual({
|
||||
r: 0,
|
||||
g: 255,
|
||||
b: 0
|
||||
})
|
||||
expect(hsbToRgb({ h: 240, s: 100, b: 100 })).toEqual({
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 255
|
||||
})
|
||||
})
|
||||
|
||||
it('handles non-100 brightness and clamps/normalizes input', () => {
|
||||
const rgb = hsbToRgb({ h: 360, s: 150, b: 50 })
|
||||
expect(rgbToHex(rgb)).toBe('#7f0000')
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('colorUtil - adjustColor', () => {
|
||||
const runAdjustColorTests = (
|
||||
color: ColorTestCase,
|
||||
format: ColorFormat
|
||||
): void => {
|
||||
it('converts lightness', () => {
|
||||
const result = adjustColor(color[format], { lightness: targetLightness })
|
||||
expect(result).toBe(color.lightExpected)
|
||||
})
|
||||
|
||||
it('applies opacity', () => {
|
||||
const result = adjustColor(color[format], { opacity: targetOpacity })
|
||||
expect(result).toBe(color.transparentExpected)
|
||||
})
|
||||
|
||||
it('applies lightness and opacity jointly', () => {
|
||||
const result = adjustColor(color[format], {
|
||||
lightness: targetLightness,
|
||||
opacity: targetOpacity
|
||||
})
|
||||
expect(result).toBe(color.lightTransparentExpected)
|
||||
})
|
||||
}
|
||||
|
||||
describe.each(Object.entries(colors))('%s color', (_colorName, color) => {
|
||||
describe.each(formats)('%s format', (format) => {
|
||||
runAdjustColorTests(color, format as ColorFormat)
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the original value for invalid color formats', () => {
|
||||
const invalidColors = [
|
||||
'cmky(100, 50, 50, 0.5)',
|
||||
'rgb(300, -10, 256)',
|
||||
'xyz(255, 255, 255)',
|
||||
'hsl(100, 50, 50%)',
|
||||
'hsl(100, 50%, 50)',
|
||||
'#GGGGGG',
|
||||
'#3333'
|
||||
]
|
||||
|
||||
invalidColors.forEach((color) => {
|
||||
const result = adjustColor(color, {
|
||||
lightness: targetLightness,
|
||||
opacity: targetOpacity
|
||||
})
|
||||
expect(result).toBe(color)
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the original value for null or undefined inputs', () => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(adjustColor(null, { opacity: targetOpacity })).toBe(null)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(adjustColor(undefined, { opacity: targetOpacity })).toBe(undefined)
|
||||
})
|
||||
|
||||
describe('handles input variations', () => {
|
||||
it('handles spaces in rgb input', () => {
|
||||
const variations = [
|
||||
'rgb(0, 0, 0)',
|
||||
'rgb(0,0,0)',
|
||||
'rgb(0, 0,0)',
|
||||
'rgb(0,0, 0)'
|
||||
]
|
||||
assertColorVariationsMatch(variations, { lightness: 0.5 })
|
||||
})
|
||||
|
||||
it('handles spaces in hsl input', () => {
|
||||
const variations = [
|
||||
'hsl(0, 0%, 0%)',
|
||||
'hsl(0,0%,0%)',
|
||||
'hsl(0, 0%,0%)',
|
||||
'hsl(0,0%, 0%)'
|
||||
]
|
||||
assertColorVariationsMatch(variations, { lightness: 0.5 })
|
||||
})
|
||||
|
||||
it('handles different decimal places in rgba input', () => {
|
||||
const variations = [
|
||||
'rgba(0, 0, 0, 0.5)',
|
||||
'rgba(0, 0, 0, 0.50)',
|
||||
'rgba(0, 0, 0, 0.500)'
|
||||
]
|
||||
assertColorVariationsMatch(variations, { opacity: 0.5 })
|
||||
})
|
||||
|
||||
it('handles different decimal places in hsla input', () => {
|
||||
const variations = [
|
||||
'hsla(0, 0%, 0%, 0.5)',
|
||||
'hsla(0, 0%, 0%, 0.50)',
|
||||
'hsla(0, 0%, 0%, 0.500)'
|
||||
]
|
||||
assertColorVariationsMatch(variations, { opacity: 0.5 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('clamps values correctly', () => {
|
||||
it('clamps lightness to 0 and 100', () => {
|
||||
expect(adjustColor('hsl(0, 100%, 50%)', { lightness: -1 })).toBe(
|
||||
'hsla(0, 100%, 0%, 1)'
|
||||
)
|
||||
expect(adjustColor('hsl(0, 100%, 50%)', { lightness: 1.5 })).toBe(
|
||||
'hsla(0, 100%, 100%, 1)'
|
||||
)
|
||||
})
|
||||
|
||||
it('clamps opacity to 0 and 1', () => {
|
||||
expect(adjustColor('rgba(0, 0, 0, 0.5)', { opacity: -0.5 })).toBe(
|
||||
'hsla(0, 0%, 0%, 0)'
|
||||
)
|
||||
expect(adjustColor('rgba(0, 0, 0, 0.5)', { opacity: 1.5 })).toBe(
|
||||
'hsla(0, 0%, 0%, 1)'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
199
src/utils/executableGroupNodeChildDTO.test.ts
Normal file
199
src/utils/executableGroupNodeChildDTO.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { GroupNodeHandler } from '@/extensions/core/groupNode'
|
||||
import type {
|
||||
ExecutableLGraphNode,
|
||||
ExecutionId,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO'
|
||||
|
||||
describe('ExecutableGroupNodeChildDTO', () => {
|
||||
let mockNode: LGraphNode
|
||||
let mockInputNode: LGraphNode
|
||||
let mockNodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>
|
||||
let mockGroupNodeHandler: GroupNodeHandler
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock nodes
|
||||
mockNode = {
|
||||
id: '3', // Simple node ID for most tests
|
||||
graph: {},
|
||||
getInputNode: vi.fn(),
|
||||
getInputLink: vi.fn(),
|
||||
inputs: []
|
||||
} as any
|
||||
|
||||
mockInputNode = {
|
||||
id: '1',
|
||||
graph: {}
|
||||
} as any
|
||||
|
||||
// Create the nodesByExecutionId map
|
||||
mockNodesByExecutionId = new Map()
|
||||
|
||||
mockGroupNodeHandler = {} as GroupNodeHandler
|
||||
})
|
||||
|
||||
describe('resolveInput', () => {
|
||||
it('should resolve input from external node (node outside the group)', () => {
|
||||
// Setup: Group node child with ID '10:3'
|
||||
const groupNodeChild = {
|
||||
id: '10:3',
|
||||
graph: {},
|
||||
getInputNode: vi.fn().mockReturnValue(mockInputNode),
|
||||
getInputLink: vi.fn().mockReturnValue({
|
||||
origin_slot: 0
|
||||
}),
|
||||
inputs: []
|
||||
} as any
|
||||
|
||||
// External node with ID '1'
|
||||
const externalNodeDto = {
|
||||
id: '1',
|
||||
type: 'TestNode'
|
||||
} as ExecutableLGraphNode
|
||||
|
||||
mockNodesByExecutionId.set('1', externalNodeDto)
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
groupNodeChild,
|
||||
[], // No subgraph path - group is in root graph
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
const result = dto.resolveInput(0)
|
||||
|
||||
expect(result).toEqual({
|
||||
node: externalNodeDto,
|
||||
origin_id: '1',
|
||||
origin_slot: 0
|
||||
})
|
||||
})
|
||||
|
||||
it('should resolve input from internal node (node inside the same group)', () => {
|
||||
// Setup: Group node child with ID '10:3'
|
||||
const groupNodeChild = {
|
||||
id: '10:3',
|
||||
graph: {},
|
||||
getInputNode: vi.fn(),
|
||||
getInputLink: vi.fn(),
|
||||
inputs: []
|
||||
} as any
|
||||
|
||||
// Internal node with ID '10:2'
|
||||
const internalInputNode = {
|
||||
id: '10:2',
|
||||
graph: {}
|
||||
} as LGraphNode
|
||||
|
||||
const internalNodeDto = {
|
||||
id: '2',
|
||||
type: 'InternalNode'
|
||||
} as ExecutableLGraphNode
|
||||
|
||||
// Internal nodes are stored with just their index
|
||||
mockNodesByExecutionId.set('2', internalNodeDto)
|
||||
|
||||
groupNodeChild.getInputNode.mockReturnValue(internalInputNode)
|
||||
groupNodeChild.getInputLink.mockReturnValue({
|
||||
origin_slot: 1
|
||||
})
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
groupNodeChild,
|
||||
[],
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
const result = dto.resolveInput(0)
|
||||
|
||||
expect(result).toEqual({
|
||||
node: internalNodeDto,
|
||||
origin_id: '10:2',
|
||||
origin_slot: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('should return undefined if no input node exists', () => {
|
||||
mockNode.getInputNode = vi.fn().mockReturnValue(null)
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
mockNode,
|
||||
[],
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
const result = dto.resolveInput(0)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should throw error if input link is missing', () => {
|
||||
mockNode.getInputNode = vi.fn().mockReturnValue(mockInputNode)
|
||||
mockNode.getInputLink = vi.fn().mockReturnValue(null)
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
mockNode,
|
||||
[],
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
expect(() => dto.resolveInput(0)).toThrow('Failed to get input link')
|
||||
})
|
||||
|
||||
it('should throw error if input node cannot be found in nodesByExecutionId', () => {
|
||||
// Node exists but is not in the map
|
||||
mockNode.getInputNode = vi.fn().mockReturnValue(mockInputNode)
|
||||
mockNode.getInputLink = vi.fn().mockReturnValue({
|
||||
origin_slot: 0
|
||||
})
|
||||
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
mockNode,
|
||||
[],
|
||||
mockNodesByExecutionId, // Empty map
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
expect(() => dto.resolveInput(0)).toThrow(
|
||||
'Failed to get input node 1 for group node child 3 with slot 0'
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error for group nodes inside subgraphs (unsupported)', () => {
|
||||
// Setup: Group node child inside a subgraph (execution ID has more than 2 segments)
|
||||
const nestedGroupNode = {
|
||||
id: '1:2:3', // subgraph:groupnode:innernode
|
||||
graph: {},
|
||||
getInputNode: vi.fn().mockReturnValue(mockInputNode),
|
||||
getInputLink: vi.fn().mockReturnValue({
|
||||
origin_slot: 0
|
||||
}),
|
||||
inputs: []
|
||||
} as any
|
||||
|
||||
// Create DTO with deeply nested path to simulate group node inside subgraph
|
||||
const dto = new ExecutableGroupNodeChildDTO(
|
||||
nestedGroupNode,
|
||||
['1', '2'], // Path indicating it's inside a subgraph then group
|
||||
mockNodesByExecutionId,
|
||||
undefined,
|
||||
mockGroupNodeHandler
|
||||
)
|
||||
|
||||
expect(() => dto.resolveInput(0)).toThrow(
|
||||
'Group nodes inside subgraphs are not supported. Please convert the group node to a subgraph instead.'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
1209
src/utils/graphTraversalUtil.test.ts
Normal file
1209
src/utils/graphTraversalUtil.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
145
src/utils/hostWhitelist.test.ts
Normal file
145
src/utils/hostWhitelist.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isHostWhitelisted, normalizeHost } from '@/utils/hostWhitelist'
|
||||
|
||||
describe('hostWhitelist utils', () => {
|
||||
describe('normalizeHost', () => {
|
||||
it.each([
|
||||
['LOCALHOST', 'localhost'],
|
||||
['localhost.', 'localhost'], // trims trailing dot
|
||||
['localhost:5173', 'localhost'], // strips :port
|
||||
['127.0.0.1:5173', '127.0.0.1'], // strips :port
|
||||
['[::1]:5173', '::1'], // strips brackets + :port
|
||||
['[::1]', '::1'], // strips brackets
|
||||
['::1', '::1'], // leaves plain IPv6
|
||||
[' [::1] ', '::1'], // trims whitespace
|
||||
['APP.LOCALHOST', 'app.localhost'], // lowercases
|
||||
['example.com.', 'example.com'], // trims trailing dot
|
||||
['[2001:db8::1]:8443', '2001:db8::1'], // IPv6 with brackets+port
|
||||
['2001:db8::1', '2001:db8::1'] // plain IPv6 stays
|
||||
])('normalizeHost(%o) -> %o', (input, expected) => {
|
||||
expect(normalizeHost(input)).toBe(expected)
|
||||
})
|
||||
|
||||
it('does not strip non-numeric suffixes (not a port pattern)', () => {
|
||||
expect(normalizeHost('example.com:abc')).toBe('example.com:abc')
|
||||
expect(normalizeHost('127.0.0.1:abc')).toBe('127.0.0.1:abc')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isHostWhitelisted', () => {
|
||||
describe('localhost label', () => {
|
||||
it.each([
|
||||
'localhost',
|
||||
'LOCALHOST',
|
||||
'localhost.',
|
||||
'localhost:5173',
|
||||
'foo.localhost',
|
||||
'Foo.Localhost',
|
||||
'sub.foo.localhost',
|
||||
'foo.localhost:5173'
|
||||
])('should allow %o', (input) => {
|
||||
expect(isHostWhitelisted(input)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'localhost.com',
|
||||
'evil-localhost',
|
||||
'notlocalhost',
|
||||
'foo.localhost.evil'
|
||||
])('should NOT allow %o', (input) => {
|
||||
expect(isHostWhitelisted(input)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('IPv4 127/8 loopback', () => {
|
||||
it.each([
|
||||
'127.0.0.1',
|
||||
'127.1.2.3',
|
||||
'127.255.255.255',
|
||||
'127.0.0.1:3000',
|
||||
'127.000.000.001', // leading zeros are still digits 0-255
|
||||
'127.0.0.1.' // trailing dot should be tolerated
|
||||
])('should allow %o', (input) => {
|
||||
expect(isHostWhitelisted(input)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'126.0.0.1',
|
||||
'127.256.0.1',
|
||||
'127.-1.0.1',
|
||||
'127.0.0.1:abc',
|
||||
'128.0.0.1',
|
||||
'192.168.1.10',
|
||||
'10.0.0.2',
|
||||
'0.0.0.0',
|
||||
'255.255.255.255',
|
||||
'127.0.0', // malformed
|
||||
'127.0.0.1.5' // malformed
|
||||
])('should NOT allow %o', (input) => {
|
||||
expect(isHostWhitelisted(input)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('IPv6 loopback ::1 (all textual forms)', () => {
|
||||
it.each([
|
||||
'::1',
|
||||
'[::1]',
|
||||
'[::1]:5173',
|
||||
'::0001',
|
||||
'0:0:0:0:0:0:0:1',
|
||||
'0000:0000:0000:0000:0000:0000:0000:0001',
|
||||
// Compressed equivalents of ::1 (with zeros compressed)
|
||||
'0:0::1',
|
||||
'0:0:0:0:0:0::1',
|
||||
'::0:1' // compressing the initial zeros (still ::1 when expanded)
|
||||
])('should allow %o', (input) => {
|
||||
expect(isHostWhitelisted(input)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'::2',
|
||||
'::',
|
||||
'::0',
|
||||
'0:0:0:0:0:0:0:2',
|
||||
'fe80::1', // link-local, not loopback
|
||||
'2001:db8::1',
|
||||
'::1:5173', // bracketless "port-like" suffix must not pass
|
||||
':::1', // invalid (triple colon)
|
||||
'0:0:0:0:0:0:::1', // invalid compression
|
||||
'[::1%25lo0]',
|
||||
'[::1%25lo0]:5173',
|
||||
'::1%25lo0'
|
||||
])('should NOT allow %o', (input) => {
|
||||
expect(isHostWhitelisted(input)).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject empty/whitespace-only input', () => {
|
||||
expect(isHostWhitelisted('')).toBe(false)
|
||||
expect(isHostWhitelisted(' ')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('comfy.org hosts', () => {
|
||||
it.each([
|
||||
'staging.comfy.org',
|
||||
'stagingcloud.comfy.org',
|
||||
'pr-123.testingcloud.comfy.org',
|
||||
'api.v2.staging.comfy.org'
|
||||
])('should allow %o', (input) => {
|
||||
expect(isHostWhitelisted(input)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'comfy.org.evil.com',
|
||||
'evil-comfy.org',
|
||||
'comfy.organization',
|
||||
'notcomfy.org',
|
||||
'comfy.org.hacker.net',
|
||||
'mycomfy.org.example.com'
|
||||
])('should NOT allow %o', (input) => {
|
||||
expect(isHostWhitelisted(input)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
173
src/utils/litegraphUtil.test.ts
Normal file
173
src/utils/litegraphUtil.test.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
compressWidgetInputSlots,
|
||||
migrateWidgetsValues
|
||||
} from '@/utils/litegraphUtil'
|
||||
|
||||
describe('migrateWidgetsValues', () => {
|
||||
it('should remove widget values for forceInput inputs', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {
|
||||
normalInput: {
|
||||
type: 'INT',
|
||||
name: 'normalInput'
|
||||
},
|
||||
forceInputField: {
|
||||
type: 'STRING',
|
||||
name: 'forceInputField',
|
||||
forceInput: true
|
||||
},
|
||||
anotherNormal: {
|
||||
type: 'FLOAT',
|
||||
name: 'anotherNormal'
|
||||
}
|
||||
}
|
||||
|
||||
const widgets: IWidget[] = [
|
||||
{ name: 'normalInput', type: 'number' },
|
||||
{ name: 'anotherNormal', type: 'number' }
|
||||
] as unknown as IWidget[]
|
||||
|
||||
const widgetValues = [42, 'dummy value', 3.14]
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual([42, 3.14])
|
||||
})
|
||||
|
||||
it('should return original values if lengths do not match', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {
|
||||
input1: {
|
||||
type: 'INT',
|
||||
name: 'input1',
|
||||
forceInput: true
|
||||
}
|
||||
}
|
||||
|
||||
const widgets: IWidget[] = []
|
||||
const widgetValues = [42, 'extra value']
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual(widgetValues)
|
||||
})
|
||||
|
||||
it('should handle empty widgets and values', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {}
|
||||
const widgets: IWidget[] = []
|
||||
const widgetValues: any[] = []
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should preserve order of non-forceInput widget values', () => {
|
||||
const inputDefs: Record<string, InputSpec> = {
|
||||
first: {
|
||||
type: 'INT',
|
||||
name: 'first'
|
||||
},
|
||||
forced: {
|
||||
type: 'STRING',
|
||||
name: 'forced',
|
||||
forceInput: true
|
||||
},
|
||||
last: {
|
||||
type: 'FLOAT',
|
||||
name: 'last'
|
||||
}
|
||||
}
|
||||
|
||||
const widgets: IWidget[] = [
|
||||
{ name: 'first', type: 'number' },
|
||||
{ name: 'last', type: 'number' }
|
||||
] as unknown as IWidget[]
|
||||
|
||||
const widgetValues = ['first value', 'dummy', 'last value']
|
||||
|
||||
const result = migrateWidgetsValues(inputDefs, widgets, widgetValues)
|
||||
expect(result).toEqual(['first value', 'last value'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('compressWidgetInputSlots', () => {
|
||||
it('should remove unconnected widget input slots', () => {
|
||||
const graph: ISerialisedGraph = {
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'foo',
|
||||
pos: [0, 0],
|
||||
size: [100, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{ widget: { name: 'foo' }, link: null, type: 'INT', name: 'foo' },
|
||||
{ widget: { name: 'bar' }, link: 2, type: 'INT', name: 'bar' },
|
||||
{ widget: { name: 'baz' }, link: null, type: 'INT', name: 'baz' }
|
||||
],
|
||||
outputs: []
|
||||
}
|
||||
],
|
||||
links: [[2, 1, 0, 1, 0, 'INT']]
|
||||
} as unknown as ISerialisedGraph
|
||||
|
||||
compressWidgetInputSlots(graph)
|
||||
|
||||
expect(graph.nodes[0].inputs).toEqual([
|
||||
{ widget: { name: 'bar' }, link: 2, type: 'INT', name: 'bar' }
|
||||
])
|
||||
})
|
||||
|
||||
it('should update link target slots correctly', () => {
|
||||
const graph: ISerialisedGraph = {
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'foo',
|
||||
pos: [0, 0],
|
||||
size: [100, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
inputs: [
|
||||
{ widget: { name: 'foo' }, link: null, type: 'INT', name: 'foo' },
|
||||
{ widget: { name: 'bar' }, link: 2, type: 'INT', name: 'bar' },
|
||||
{ widget: { name: 'baz' }, link: 3, type: 'INT', name: 'baz' }
|
||||
],
|
||||
outputs: []
|
||||
}
|
||||
],
|
||||
links: [
|
||||
[2, 1, 0, 1, 1, 'INT'],
|
||||
[3, 1, 0, 1, 2, 'INT']
|
||||
]
|
||||
} as unknown as ISerialisedGraph
|
||||
|
||||
compressWidgetInputSlots(graph)
|
||||
|
||||
expect(graph.nodes[0].inputs).toEqual([
|
||||
{ widget: { name: 'bar' }, link: 2, type: 'INT', name: 'bar' },
|
||||
{ widget: { name: 'baz' }, link: 3, type: 'INT', name: 'baz' }
|
||||
])
|
||||
|
||||
expect(graph.links).toEqual([
|
||||
[2, 1, 0, 1, 0, 'INT'],
|
||||
[3, 1, 0, 1, 1, 'INT']
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle graphs with no nodes gracefully', () => {
|
||||
const graph: ISerialisedGraph = {
|
||||
nodes: [],
|
||||
links: []
|
||||
} as unknown as ISerialisedGraph
|
||||
|
||||
compressWidgetInputSlots(graph)
|
||||
|
||||
expect(graph.nodes).toEqual([])
|
||||
expect(graph.links).toEqual([])
|
||||
})
|
||||
})
|
||||
123
src/utils/markdownRendererUtil.test.ts
Normal file
123
src/utils/markdownRendererUtil.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'
|
||||
|
||||
describe('markdownRendererUtil', () => {
|
||||
describe('renderMarkdownToHtml', () => {
|
||||
it('should render basic markdown to HTML', () => {
|
||||
const markdown = '# Hello\n\nThis is a test.'
|
||||
const html = renderMarkdownToHtml(markdown)
|
||||
|
||||
expect(html).toContain('<h1')
|
||||
expect(html).toContain('Hello')
|
||||
expect(html).toContain('<p>')
|
||||
expect(html).toContain('This is a test.')
|
||||
})
|
||||
|
||||
it('should render links with target="_blank" and rel="noopener noreferrer"', () => {
|
||||
const markdown = '[Click here](https://example.com)'
|
||||
const html = renderMarkdownToHtml(markdown)
|
||||
|
||||
expect(html).toContain('target="_blank"')
|
||||
expect(html).toContain('rel="noopener noreferrer"')
|
||||
expect(html).toContain('href="https://example.com"')
|
||||
expect(html).toContain('Click here')
|
||||
})
|
||||
|
||||
it('should render multiple links with target="_blank"', () => {
|
||||
const markdown =
|
||||
'[Link 1](https://example.com) and [Link 2](https://test.com)'
|
||||
const html = renderMarkdownToHtml(markdown)
|
||||
|
||||
const targetBlankMatches = html.match(/target="_blank"/g)
|
||||
expect(targetBlankMatches).toHaveLength(2)
|
||||
|
||||
const relMatches = html.match(/rel="noopener noreferrer"/g)
|
||||
expect(relMatches).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should handle relative image paths with baseUrl', () => {
|
||||
const markdown = ''
|
||||
const baseUrl = 'https://cdn.example.com'
|
||||
const html = renderMarkdownToHtml(markdown, baseUrl)
|
||||
|
||||
expect(html).toContain(`src="${baseUrl}/image.png"`)
|
||||
expect(html).toContain('alt="Alt text"')
|
||||
})
|
||||
|
||||
it('should not modify absolute image URLs', () => {
|
||||
const markdown = ''
|
||||
const baseUrl = 'https://cdn.example.com'
|
||||
const html = renderMarkdownToHtml(markdown, baseUrl)
|
||||
|
||||
expect(html).toContain('src="https://example.com/image.png"')
|
||||
expect(html).not.toContain(baseUrl)
|
||||
})
|
||||
|
||||
it('should handle empty markdown', () => {
|
||||
const html = renderMarkdownToHtml('')
|
||||
|
||||
expect(html).toBe('')
|
||||
})
|
||||
|
||||
it('should sanitize potentially dangerous HTML', () => {
|
||||
const markdown = '<script>alert("xss")</script>'
|
||||
const html = renderMarkdownToHtml(markdown)
|
||||
|
||||
expect(html).not.toContain('<script>')
|
||||
expect(html).not.toContain('alert')
|
||||
})
|
||||
|
||||
it('should allow video tags with proper attributes', () => {
|
||||
const markdown =
|
||||
'<video src="video.mp4" controls autoplay loop muted></video>'
|
||||
const html = renderMarkdownToHtml(markdown)
|
||||
|
||||
expect(html).toContain('<video')
|
||||
expect(html).toContain('src="video.mp4"')
|
||||
expect(html).toContain('controls')
|
||||
})
|
||||
|
||||
it('should render links with title attribute', () => {
|
||||
const markdown = '[Link](https://example.com "This is a title")'
|
||||
const html = renderMarkdownToHtml(markdown)
|
||||
|
||||
expect(html).toContain('title="This is a title"')
|
||||
expect(html).toContain('target="_blank"')
|
||||
expect(html).toContain('rel="noopener noreferrer"')
|
||||
})
|
||||
|
||||
it('should handle bare URLs (autolinks)', () => {
|
||||
const markdown = 'Visit https://example.com for more info.'
|
||||
const html = renderMarkdownToHtml(markdown)
|
||||
|
||||
expect(html).toContain('href="https://example.com"')
|
||||
expect(html).toContain('target="_blank"')
|
||||
expect(html).toContain('rel="noopener noreferrer"')
|
||||
})
|
||||
|
||||
it('should render complex markdown with links, images, and text', () => {
|
||||
const markdown = `
|
||||
# Release Notes
|
||||
|
||||
Check out our [documentation](https://docs.example.com) for more info.
|
||||
|
||||

|
||||
|
||||
Visit our [homepage](https://example.com) to learn more.
|
||||
`
|
||||
const baseUrl = 'https://cdn.example.com'
|
||||
const html = renderMarkdownToHtml(markdown, baseUrl)
|
||||
|
||||
// Check links have target="_blank"
|
||||
const targetBlankMatches = html.match(/target="_blank"/g)
|
||||
expect(targetBlankMatches).toHaveLength(2)
|
||||
|
||||
// Check image has baseUrl prepended
|
||||
expect(html).toContain(`${baseUrl}/screenshot.png`)
|
||||
|
||||
// Check heading
|
||||
expect(html).toContain('Release Notes')
|
||||
})
|
||||
})
|
||||
})
|
||||
110
src/utils/mathUtil.test.ts
Normal file
110
src/utils/mathUtil.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||
import { computeUnionBounds, gcd, lcm } from '@/utils/mathUtil'
|
||||
|
||||
describe('mathUtil', () => {
|
||||
describe('gcd', () => {
|
||||
it('should compute greatest common divisor correctly', () => {
|
||||
expect(gcd(48, 18)).toBe(6)
|
||||
expect(gcd(100, 25)).toBe(25)
|
||||
expect(gcd(17, 13)).toBe(1)
|
||||
expect(gcd(0, 5)).toBe(5)
|
||||
expect(gcd(5, 0)).toBe(5)
|
||||
})
|
||||
|
||||
it('should handle negative numbers', () => {
|
||||
expect(gcd(-48, 18)).toBe(6)
|
||||
expect(gcd(48, -18)).toBe(6)
|
||||
expect(gcd(-48, -18)).toBe(6)
|
||||
})
|
||||
|
||||
it('should not cause stack overflow with small floating-point step values', () => {
|
||||
// This would cause Maximum call stack size exceeded with recursive impl
|
||||
// when used in lcm calculations with small step values
|
||||
expect(() => gcd(0.0001, 0.0003)).not.toThrow()
|
||||
expect(() => gcd(1e-10, 1e-9)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('lcm', () => {
|
||||
it('should compute least common multiple correctly', () => {
|
||||
expect(lcm(4, 6)).toBe(12)
|
||||
expect(lcm(15, 20)).toBe(60)
|
||||
expect(lcm(7, 11)).toBe(77)
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeUnionBounds', () => {
|
||||
it('should return null for empty input', () => {
|
||||
expect(computeUnionBounds([])).toBe(null)
|
||||
})
|
||||
|
||||
// Tests for tuple format (ReadOnlyRect)
|
||||
it('should work with ReadOnlyRect tuple format', () => {
|
||||
const tuples: ReadOnlyRect[] = [
|
||||
[10, 20, 30, 40] as const, // bounds: 10,20 to 40,60
|
||||
[50, 10, 20, 30] as const // bounds: 50,10 to 70,40
|
||||
]
|
||||
|
||||
const result = computeUnionBounds(tuples)
|
||||
|
||||
expect(result).toEqual({
|
||||
x: 10, // min(10, 50)
|
||||
y: 10, // min(20, 10)
|
||||
width: 60, // max(40, 70) - min(10, 50) = 70 - 10
|
||||
height: 50 // max(60, 40) - min(20, 10) = 60 - 10
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle single ReadOnlyRect tuple', () => {
|
||||
const tuple: ReadOnlyRect = [10, 20, 30, 40] as const
|
||||
const result = computeUnionBounds([tuple])
|
||||
|
||||
expect(result).toEqual({
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 30,
|
||||
height: 40
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle tuple format with negative dimensions', () => {
|
||||
const tuples: ReadOnlyRect[] = [
|
||||
[100, 50, -20, -10] as const, // x+width=80, y+height=40
|
||||
[90, 45, 15, 20] as const // x+width=105, y+height=65
|
||||
]
|
||||
|
||||
const result = computeUnionBounds(tuples)
|
||||
|
||||
expect(result).toEqual({
|
||||
x: 90, // min(100, 90)
|
||||
y: 45, // min(50, 45)
|
||||
width: 15, // max(80, 105) - min(100, 90) = 105 - 90
|
||||
height: 20 // max(40, 65) - min(50, 45) = 65 - 45
|
||||
})
|
||||
})
|
||||
|
||||
it('should maintain optimal performance with SoA tuples', () => {
|
||||
// Test that array access is as expected for typical selection sizes
|
||||
const tuples: ReadOnlyRect[] = Array.from(
|
||||
{ length: 10 },
|
||||
(_, i) =>
|
||||
[
|
||||
i * 20, // x
|
||||
i * 15, // y
|
||||
100 + i * 5, // width
|
||||
80 + i * 3 // height
|
||||
] as const
|
||||
)
|
||||
|
||||
const result = computeUnionBounds(tuples)
|
||||
|
||||
expect(result).toBeTruthy()
|
||||
expect(result!.x).toBe(0)
|
||||
expect(result!.y).toBe(0)
|
||||
expect(result!.width).toBe(325)
|
||||
expect(result!.height).toBe(242)
|
||||
})
|
||||
})
|
||||
})
|
||||
288
src/utils/migration/__fixtures__/reroute/legacy/branching.json
Normal file
288
src/utils/migration/__fixtures__/reroute/legacy/branching.json
Normal file
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"last_node_id": 27,
|
||||
"last_link_id": 34,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
620,
|
||||
260
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
47.948699951171875,
|
||||
239.2628173828125
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
13,
|
||||
31
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
510,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 32
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
404.7915344238281,
|
||||
280.9454650878906
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 31
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
32,
|
||||
33
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
514,
|
||||
386
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 33
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
34
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
625,
|
||||
373
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 34
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
21,
|
||||
13,
|
||||
0,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
31,
|
||||
4,
|
||||
2,
|
||||
25,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
32,
|
||||
25,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
33,
|
||||
25,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
34,
|
||||
27,
|
||||
0,
|
||||
26,
|
||||
1,
|
||||
"VAE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 2.3195551508147507,
|
||||
"offset": [
|
||||
96.55985005696607,
|
||||
-41.449812921703376
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
343
src/utils/migration/__fixtures__/reroute/legacy/floating.json
Normal file
343
src/utils/migration/__fixtures__/reroute/legacy/floating.json
Normal file
@@ -0,0 +1,343 @@
|
||||
{
|
||||
"last_node_id": 31,
|
||||
"last_link_id": 38,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
47.948699951171875,
|
||||
239.2628173828125
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
13,
|
||||
31
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
510,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
620,
|
||||
260
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
401.49267578125,
|
||||
278.96600341796875
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 31
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
401.874267578125,
|
||||
354.56207275390625
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
533.9331665039062,
|
||||
433.1788330078125
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 37
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
535.5305786132812,
|
||||
350.32513427734375
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 38
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
405.2763671875,
|
||||
431.85943603515625
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"links": [
|
||||
37,
|
||||
38
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
21,
|
||||
13,
|
||||
0,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
31,
|
||||
4,
|
||||
2,
|
||||
25,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
37,
|
||||
31,
|
||||
0,
|
||||
30,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
38,
|
||||
31,
|
||||
0,
|
||||
29,
|
||||
0,
|
||||
"*"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.2526143885641918,
|
||||
"offset": [
|
||||
449.14488520559655,
|
||||
200.06933385457722
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"last_node_id": 36,
|
||||
"last_link_id": 44,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 33,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
492.768310546875,
|
||||
274.761962890625
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 41
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
40
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
362.8304138183594,
|
||||
275.12872314453125
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 39
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": [
|
||||
41,
|
||||
42
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
-0.6348835229873657,
|
||||
238.0631866455078
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
39
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
611.6028442382812,
|
||||
254.6018524169922
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 40
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
490.8152770996094,
|
||||
364.4836730957031
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 42
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
39,
|
||||
4,
|
||||
2,
|
||||
32,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
40,
|
||||
33,
|
||||
0,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
41,
|
||||
32,
|
||||
0,
|
||||
33,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
42,
|
||||
32,
|
||||
0,
|
||||
34,
|
||||
0,
|
||||
"*"
|
||||
]
|
||||
],
|
||||
"floatingLinks": [
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 2,
|
||||
"target_id": -1,
|
||||
"target_slot": -1,
|
||||
"type": "*"
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.6672297511789418,
|
||||
"offset": [
|
||||
262.0504372113823,
|
||||
124.35120995663942
|
||||
]
|
||||
},
|
||||
"linkExtensions": []
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"last_node_id": 24,
|
||||
"last_link_id": 30,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
620,
|
||||
260
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "Reroute",
|
||||
"pos": [
|
||||
510,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
75,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "*",
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "VAE",
|
||||
"slot_index": 0,
|
||||
"links": [
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"showOutputText": false,
|
||||
"horizontal": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
160,
|
||||
240
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
13
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
13,
|
||||
4,
|
||||
2,
|
||||
13,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
21,
|
||||
13,
|
||||
0,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 2.7130434782608694,
|
||||
"offset": [
|
||||
-35,
|
||||
-40.86698717948718
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
202
src/utils/migration/__fixtures__/reroute/native/branching.json
Normal file
202
src/utils/migration/__fixtures__/reroute/native/branching.json
Normal file
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"last_node_id": 27,
|
||||
"last_link_id": 34,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
47.948699951171875,
|
||||
239.2628173828125
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
21,
|
||||
34
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
620,
|
||||
260
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
625,
|
||||
373
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 34
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
21,
|
||||
4,
|
||||
2,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
34,
|
||||
4,
|
||||
2,
|
||||
26,
|
||||
1,
|
||||
"VAE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 2.3195551508147507,
|
||||
"offset": [
|
||||
96.55985005696607,
|
||||
-41.449812921703376
|
||||
]
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [
|
||||
547.5,
|
||||
293
|
||||
],
|
||||
"linkIds": [
|
||||
21
|
||||
],
|
||||
"parentId": 2
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"pos": [
|
||||
442.2915344238281,
|
||||
293.9454650878906
|
||||
],
|
||||
"linkIds": [
|
||||
21,
|
||||
34
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"pos": [
|
||||
551.5,
|
||||
399
|
||||
],
|
||||
"linkIds": [
|
||||
34
|
||||
],
|
||||
"parentId": 2
|
||||
}
|
||||
],
|
||||
"linkExtensions": [
|
||||
{
|
||||
"id": 21,
|
||||
"parentId": 1
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"parentId": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
148
src/utils/migration/__fixtures__/reroute/native/floating.json
Normal file
148
src/utils/migration/__fixtures__/reroute/native/floating.json
Normal file
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"last_node_id": 31,
|
||||
"last_link_id": 38,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
47.948699951171875,
|
||||
239.2628173828125
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
620,
|
||||
260
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.2526143885641918,
|
||||
"offset": [
|
||||
449.14488520559655,
|
||||
200.06933385457722
|
||||
]
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [
|
||||
547.5,
|
||||
293
|
||||
],
|
||||
"linkIds": [],
|
||||
"floating": {
|
||||
"slotType": "input"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"pos": [
|
||||
438.99267578125,
|
||||
291.96600341796875
|
||||
],
|
||||
"linkIds": [],
|
||||
"floating": {
|
||||
"slotType": "output"
|
||||
}
|
||||
}
|
||||
],
|
||||
"linkExtensions": []
|
||||
},
|
||||
"version": 0.4,
|
||||
"floatingLinks": [
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": -1,
|
||||
"origin_slot": -1,
|
||||
"target_id": 12,
|
||||
"target_slot": 1,
|
||||
"type": "VAE",
|
||||
"parentId": 1
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 2,
|
||||
"target_id": -1,
|
||||
"target_slot": -1,
|
||||
"type": "*",
|
||||
"parentId": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"last_node_id": 36,
|
||||
"last_link_id": 44,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
-0.6348835229873657,
|
||||
238.0631866455078
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
40
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
611.6028442382812,
|
||||
254.6018524169922
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 40
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.26",
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
40,
|
||||
4,
|
||||
2,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
]
|
||||
],
|
||||
"floatingLinks": [
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 2,
|
||||
"target_id": -1,
|
||||
"target_slot": -1,
|
||||
"type": "*",
|
||||
"parentId": 3
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.6672297511789418,
|
||||
"offset": [
|
||||
262.0504372113823,
|
||||
124.35120995663942
|
||||
]
|
||||
},
|
||||
"linkExtensions": [
|
||||
{
|
||||
"id": 40,
|
||||
"parentId": 1
|
||||
}
|
||||
],
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [
|
||||
530.268310546875,
|
||||
287.761962890625
|
||||
],
|
||||
"linkIds": [
|
||||
40
|
||||
],
|
||||
"parentId": 2
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"pos": [
|
||||
400.3304138183594,
|
||||
288.12872314453125
|
||||
],
|
||||
"linkIds": [
|
||||
40
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"pos": [
|
||||
528.3152770996094,
|
||||
377.4836730957031
|
||||
],
|
||||
"linkIds": [],
|
||||
"parentId": 2,
|
||||
"floating": {
|
||||
"slotType": "output"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"last_node_id": 24,
|
||||
"last_link_id": 30,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [
|
||||
160,
|
||||
240
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
98
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"slot_index": 0,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"slot_index": 1,
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"slot_index": 2,
|
||||
"links": [
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
620,
|
||||
260
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 21
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
21,
|
||||
4,
|
||||
2,
|
||||
12,
|
||||
1,
|
||||
"VAE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 2.7130434782608694,
|
||||
"offset": [
|
||||
-35,
|
||||
-40.86698717948718
|
||||
]
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
"pos": [
|
||||
547.5,
|
||||
293
|
||||
],
|
||||
"linkIds": [
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"linkExtensions": [
|
||||
{
|
||||
"id": 21,
|
||||
"parentId": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
37
src/utils/migration/migrateReroute.test.ts
Normal file
37
src/utils/migration/migrateReroute.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { WorkflowJSON04 } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute'
|
||||
|
||||
describe('migrateReroute', () => {
|
||||
describe('migrateReroute snapshots', () => {
|
||||
// Helper function to load workflow JSON files
|
||||
const loadWorkflow = (filePath: string): WorkflowJSON04 => {
|
||||
const fullPath = path.resolve(__dirname, filePath)
|
||||
const fileContent = fs.readFileSync(fullPath, 'utf-8')
|
||||
return JSON.parse(fileContent) as WorkflowJSON04
|
||||
}
|
||||
|
||||
it.each([
|
||||
'branching.json',
|
||||
'single_connected.json',
|
||||
'floating.json',
|
||||
'floating_branch.json'
|
||||
])('should correctly migrate %s', async (fileName) => {
|
||||
// Load the legacy workflow
|
||||
const legacyWorkflow = loadWorkflow(
|
||||
`__fixtures__/reroute/legacy/${fileName}`
|
||||
)
|
||||
|
||||
// Migrate the workflow
|
||||
const migratedWorkflow = migrateLegacyRerouteNodes(legacyWorkflow)
|
||||
|
||||
// Compare with snapshot
|
||||
await expect(
|
||||
JSON.stringify(migratedWorkflow, null, 2)
|
||||
).toMatchFileSnapshot(`__fixtures__/reroute/native/${fileName}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
223
src/utils/nodeDefUtil.test.ts
Normal file
223
src/utils/nodeDefUtil.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type {
|
||||
ComboInputSpec,
|
||||
ComboInputSpecV2,
|
||||
FloatInputSpec,
|
||||
InputSpec,
|
||||
IntInputSpec
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { mergeInputSpec } from '@/utils/nodeDefUtil'
|
||||
|
||||
describe('nodeDefUtil', () => {
|
||||
describe('mergeInputSpec', () => {
|
||||
// Test numeric input specs (INT and FLOAT)
|
||||
describe('numeric input specs', () => {
|
||||
it('should merge INT specs with overlapping ranges', () => {
|
||||
const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
|
||||
const spec2: IntInputSpec = ['INT', { min: 5, max: 15 }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('INT')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].min).toBe(5)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].max).toBe(10)
|
||||
})
|
||||
|
||||
it('should return null for INT specs with non-overlapping ranges', () => {
|
||||
const spec1: IntInputSpec = ['INT', { min: 0, max: 5 }]
|
||||
const spec2: IntInputSpec = ['INT', { min: 10, max: 15 }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should merge FLOAT specs with overlapping ranges', () => {
|
||||
const spec1: FloatInputSpec = ['FLOAT', { min: 0.5, max: 10.5 }]
|
||||
const spec2: FloatInputSpec = ['FLOAT', { min: 5.5, max: 15.5 }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('FLOAT')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].min).toBe(5.5)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].max).toBe(10.5)
|
||||
})
|
||||
|
||||
it('should handle specs with undefined min/max values', () => {
|
||||
const spec1: FloatInputSpec = ['FLOAT', { min: 0.5 }]
|
||||
const spec2: FloatInputSpec = ['FLOAT', { max: 15.5 }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('FLOAT')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].min).toBe(0.5)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].max).toBe(15.5)
|
||||
})
|
||||
|
||||
it('should merge step values using least common multiple', () => {
|
||||
const spec1: IntInputSpec = ['INT', { min: 0, max: 10, step: 2 }]
|
||||
const spec2: IntInputSpec = ['INT', { min: 0, max: 10, step: 3 }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('INT')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].step).toBe(6) // LCM of 2 and 3 is 6
|
||||
})
|
||||
|
||||
it('should use default step of 1 when step is not specified', () => {
|
||||
const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
|
||||
const spec2: IntInputSpec = ['INT', { min: 0, max: 10, step: 4 }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('INT')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].step).toBe(4) // LCM of 1 and 4 is 4
|
||||
})
|
||||
|
||||
it('should handle step values for FLOAT specs', () => {
|
||||
const spec1: FloatInputSpec = ['FLOAT', { min: 0, max: 10, step: 0.5 }]
|
||||
const spec2: FloatInputSpec = ['FLOAT', { min: 0, max: 10, step: 0.25 }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('FLOAT')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].step).toBe(0.5)
|
||||
})
|
||||
})
|
||||
|
||||
// Test combo input specs
|
||||
describe('combo input specs', () => {
|
||||
it('should merge COMBO specs with overlapping options', () => {
|
||||
const spec1: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B', 'C'] }]
|
||||
const spec2: ComboInputSpecV2 = ['COMBO', { options: ['B', 'C', 'D'] }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('COMBO')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].options).toEqual(['B', 'C'])
|
||||
})
|
||||
|
||||
it('should return null for COMBO specs with no overlapping options', () => {
|
||||
const spec1: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B'] }]
|
||||
const spec2: ComboInputSpecV2 = ['COMBO', { options: ['C', 'D'] }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should handle COMBO specs with additional properties', () => {
|
||||
const spec1: ComboInputSpecV2 = [
|
||||
'COMBO',
|
||||
{
|
||||
options: ['A', 'B', 'C'],
|
||||
default: 'A',
|
||||
tooltip: 'Select an option'
|
||||
}
|
||||
]
|
||||
const spec2: ComboInputSpecV2 = [
|
||||
'COMBO',
|
||||
{
|
||||
options: ['B', 'C', 'D'],
|
||||
default: 'B',
|
||||
multiline: true
|
||||
}
|
||||
]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('COMBO')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].options).toEqual(['B', 'C'])
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].default).toBe('B')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].tooltip).toBe('Select an option')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].multiline).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle v1 and v2 combo specs', () => {
|
||||
const spec1: ComboInputSpec = [['A', 'B', 'C', 'D'], {}]
|
||||
const spec2: ComboInputSpecV2 = ['COMBO', { options: ['C', 'D'] }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('COMBO')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].options).toEqual(['C', 'D'])
|
||||
})
|
||||
})
|
||||
|
||||
// Test common input spec behavior
|
||||
describe('common input spec behavior', () => {
|
||||
it('should return null for specs with different types', () => {
|
||||
const spec1: IntInputSpec = ['INT', { min: 0, max: 10 }]
|
||||
const spec2: ComboInputSpecV2 = ['COMBO', { options: ['A', 'B'] }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2 as unknown as IntInputSpec)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should ignore specified keys when comparing specs', () => {
|
||||
const spec1: InputSpec = [
|
||||
'STRING',
|
||||
{
|
||||
default: 'value1',
|
||||
tooltip: 'Tooltip 1',
|
||||
step: 1
|
||||
}
|
||||
]
|
||||
const spec2: InputSpec = [
|
||||
'STRING',
|
||||
{
|
||||
default: 'value2',
|
||||
tooltip: 'Tooltip 2',
|
||||
step: 1
|
||||
}
|
||||
]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.[0]).toBe('STRING')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].default).toBe('value2')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].tooltip).toBe('Tooltip 2')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
expect(result?.[1].step).toBe(1)
|
||||
})
|
||||
|
||||
it('should return null if non-ignored properties differ', () => {
|
||||
const spec1: InputSpec = ['STRING', { step: 1 }]
|
||||
const spec2: InputSpec = ['STRING', { step: 2 }]
|
||||
|
||||
const result = mergeInputSpec(spec1, spec2)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
114
src/utils/nodeFilterUtil.test.ts
Normal file
114
src/utils/nodeFilterUtil.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { filterOutputNodes, isOutputNode } from '@/utils/nodeFilterUtil'
|
||||
|
||||
describe('nodeFilterUtil', () => {
|
||||
// Helper to create a mock node
|
||||
const createMockNode = (
|
||||
id: number,
|
||||
isOutputNode: boolean = false
|
||||
): LGraphNode => {
|
||||
// Create a custom class with the nodeData static property
|
||||
class MockNode extends LGraphNode {
|
||||
static nodeData = isOutputNode ? { output_node: true } : {}
|
||||
}
|
||||
|
||||
const node = new MockNode('')
|
||||
node.id = id
|
||||
return node
|
||||
}
|
||||
|
||||
describe('filterOutputNodes', () => {
|
||||
it('should return empty array when given empty array', () => {
|
||||
const result = filterOutputNodes([])
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should filter out non-output nodes', () => {
|
||||
const nodes = [
|
||||
createMockNode(1, false),
|
||||
createMockNode(2, true),
|
||||
createMockNode(3, false),
|
||||
createMockNode(4, true)
|
||||
]
|
||||
|
||||
const result = filterOutputNodes(nodes)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((n) => n.id)).toEqual([2, 4])
|
||||
})
|
||||
|
||||
it('should return all nodes if all are output nodes', () => {
|
||||
const nodes = [
|
||||
createMockNode(1, true),
|
||||
createMockNode(2, true),
|
||||
createMockNode(3, true)
|
||||
]
|
||||
|
||||
const result = filterOutputNodes(nodes)
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result).toEqual(nodes)
|
||||
})
|
||||
|
||||
it('should return empty array if no output nodes', () => {
|
||||
const nodes = [
|
||||
createMockNode(1, false),
|
||||
createMockNode(2, false),
|
||||
createMockNode(3, false)
|
||||
]
|
||||
|
||||
const result = filterOutputNodes(nodes)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle nodes without nodeData', () => {
|
||||
// Create a plain LGraphNode without custom constructor
|
||||
const node = new LGraphNode('')
|
||||
node.id = 1
|
||||
|
||||
const result = filterOutputNodes([node])
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle nodes with undefined output_node', () => {
|
||||
class MockNodeWithOtherData extends LGraphNode {
|
||||
static nodeData = { someOtherProperty: true }
|
||||
}
|
||||
|
||||
const node = new MockNodeWithOtherData('')
|
||||
node.id = 1
|
||||
|
||||
const result = filterOutputNodes([node])
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isOutputNode', () => {
|
||||
it('should filter selected nodes to only output nodes', () => {
|
||||
const selectedNodes = [
|
||||
createMockNode(1, false),
|
||||
createMockNode(2, true),
|
||||
createMockNode(3, false),
|
||||
createMockNode(4, true),
|
||||
createMockNode(5, false)
|
||||
]
|
||||
|
||||
const result = selectedNodes.filter(isOutputNode)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((n) => n.id)).toEqual([2, 4])
|
||||
})
|
||||
|
||||
it('should handle empty selection', () => {
|
||||
const emptyNodes: LGraphNode[] = []
|
||||
const result = emptyNodes.filter(isOutputNode)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle selection with no output nodes', () => {
|
||||
const selectedNodes = [createMockNode(1, false), createMockNode(2, false)]
|
||||
|
||||
const result = selectedNodes.filter(isOutputNode)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
254
src/utils/packUtils.test.ts
Normal file
254
src/utils/packUtils.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { normalizePackId, normalizePackKeys } from '@/utils/packUtils'
|
||||
|
||||
describe('packUtils', () => {
|
||||
describe('normalizePackId', () => {
|
||||
it('should return pack ID unchanged when no version suffix exists', () => {
|
||||
expect(normalizePackId('ComfyUI-GGUF')).toBe('ComfyUI-GGUF')
|
||||
expect(normalizePackId('ComfyUI-Manager')).toBe('ComfyUI-Manager')
|
||||
expect(normalizePackId('simple-pack')).toBe('simple-pack')
|
||||
})
|
||||
|
||||
it('should remove version suffix with underscores', () => {
|
||||
expect(normalizePackId('ComfyUI-GGUF@1_1_4')).toBe('ComfyUI-GGUF')
|
||||
expect(normalizePackId('ComfyUI-Manager@2_0_0')).toBe('ComfyUI-Manager')
|
||||
expect(normalizePackId('pack@1_0_0_beta')).toBe('pack')
|
||||
})
|
||||
|
||||
it('should remove version suffix with dots', () => {
|
||||
expect(normalizePackId('ComfyUI-GGUF@1.1.4')).toBe('ComfyUI-GGUF')
|
||||
expect(normalizePackId('pack@2.0.0')).toBe('pack')
|
||||
})
|
||||
|
||||
it('should handle multiple @ symbols by only removing after first @', () => {
|
||||
expect(normalizePackId('pack@1_0_0@extra')).toBe('pack')
|
||||
expect(normalizePackId('my@pack@1_0_0')).toBe('my')
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(normalizePackId('')).toBe('')
|
||||
})
|
||||
|
||||
it('should handle pack ID with @ but no version', () => {
|
||||
expect(normalizePackId('pack@')).toBe('pack')
|
||||
})
|
||||
|
||||
it('should handle special characters in pack name', () => {
|
||||
expect(normalizePackId('my-pack_v2@1_0_0')).toBe('my-pack_v2')
|
||||
expect(normalizePackId('pack.with.dots@2_0_0')).toBe('pack.with.dots')
|
||||
expect(normalizePackId('UPPERCASE-Pack@1_0_0')).toBe('UPPERCASE-Pack')
|
||||
})
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
// Only @ symbol
|
||||
expect(normalizePackId('@')).toBe('')
|
||||
expect(normalizePackId('@1_0_0')).toBe('')
|
||||
|
||||
// Whitespace
|
||||
expect(normalizePackId(' pack @1_0_0')).toBe(' pack ')
|
||||
expect(normalizePackId('pack @1_0_0')).toBe('pack ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizePackKeys', () => {
|
||||
it('should normalize all keys with version suffixes', () => {
|
||||
const input = {
|
||||
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
|
||||
'ComfyUI-Manager@2_0_0': { ver: '2.0.0', enabled: false },
|
||||
'another-pack@1_0_0': { ver: '1.0.0', enabled: true }
|
||||
}
|
||||
|
||||
const expected = {
|
||||
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
|
||||
'ComfyUI-Manager': { ver: '2.0.0', enabled: false },
|
||||
'another-pack': { ver: '1.0.0', enabled: true }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should handle empty object', () => {
|
||||
expect(normalizePackKeys({})).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle keys without version suffixes', () => {
|
||||
const input = {
|
||||
pack1: { data: 'value1' },
|
||||
pack2: { data: 'value2' }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(input)
|
||||
})
|
||||
|
||||
it('should handle mixed keys (with and without versions)', () => {
|
||||
const input = {
|
||||
'normal-pack': { ver: '1.0.0' },
|
||||
'versioned-pack@2_0_0': { ver: '2.0.0' },
|
||||
'another-normal': { ver: '3.0.0' },
|
||||
'another-versioned@4_0_0': { ver: '4.0.0' }
|
||||
}
|
||||
|
||||
const expected = {
|
||||
'normal-pack': { ver: '1.0.0' },
|
||||
'versioned-pack': { ver: '2.0.0' },
|
||||
'another-normal': { ver: '3.0.0' },
|
||||
'another-versioned': { ver: '4.0.0' }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should handle duplicate keys after normalization (last one wins)', () => {
|
||||
const input = {
|
||||
'pack@1_0_0': { ver: '1.0.0', data: 'first' },
|
||||
'pack@2_0_0': { ver: '2.0.0', data: 'second' },
|
||||
pack: { ver: '3.0.0', data: 'third' }
|
||||
}
|
||||
|
||||
const result = normalizePackKeys(input)
|
||||
|
||||
// The exact behavior depends on object iteration order,
|
||||
// but there should only be one 'pack' key in the result
|
||||
expect(Object.keys(result)).toEqual(['pack'])
|
||||
expect(result.pack).toBeDefined()
|
||||
expect(result.pack.ver).toBeDefined()
|
||||
})
|
||||
|
||||
it('should preserve value references', () => {
|
||||
const value1 = { ver: '1.0.0', complex: { nested: 'data' } }
|
||||
const value2 = { ver: '2.0.0', complex: { nested: 'data2' } }
|
||||
|
||||
const input = {
|
||||
'pack1@1_0_0': value1,
|
||||
'pack2@2_0_0': value2
|
||||
}
|
||||
|
||||
const result = normalizePackKeys(input)
|
||||
|
||||
// Values should be the same references, not cloned
|
||||
expect(result.pack1).toBe(value1)
|
||||
expect(result.pack2).toBe(value2)
|
||||
})
|
||||
|
||||
it('should handle special characters in keys', () => {
|
||||
const input = {
|
||||
'@1_0_0': { ver: '1.0.0' },
|
||||
'my-pack.v2@2_0_0': { ver: '2.0.0' },
|
||||
'UPPERCASE@3_0_0': { ver: '3.0.0' }
|
||||
}
|
||||
|
||||
const expected = {
|
||||
'': { ver: '1.0.0' },
|
||||
'my-pack.v2': { ver: '2.0.0' },
|
||||
UPPERCASE: { ver: '3.0.0' }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should work with different value types', () => {
|
||||
const input = {
|
||||
'pack1@1_0_0': 'string value',
|
||||
'pack2@2_0_0': 123,
|
||||
'pack3@3_0_0': null,
|
||||
'pack4@4_0_0': undefined,
|
||||
'pack5@5_0_0': true,
|
||||
pack6: []
|
||||
}
|
||||
|
||||
const expected = {
|
||||
pack1: 'string value',
|
||||
pack2: 123,
|
||||
pack3: null,
|
||||
pack4: undefined,
|
||||
pack5: true,
|
||||
pack6: []
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration scenarios from JSDoc examples', () => {
|
||||
it('should handle the examples from normalizePackId JSDoc', () => {
|
||||
expect(normalizePackId('ComfyUI-GGUF')).toBe('ComfyUI-GGUF')
|
||||
expect(normalizePackId('ComfyUI-GGUF@1_1_4')).toBe('ComfyUI-GGUF')
|
||||
})
|
||||
|
||||
it('should handle the examples from normalizePackKeys JSDoc', () => {
|
||||
const input = {
|
||||
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
|
||||
'ComfyUI-Manager@2_0_0': { ver: '2.0.0', enabled: false }
|
||||
}
|
||||
|
||||
const expected = {
|
||||
'ComfyUI-GGUF': { ver: '1.1.4', enabled: true },
|
||||
'ComfyUI-Manager': { ver: '2.0.0', enabled: false }
|
||||
}
|
||||
|
||||
expect(normalizePackKeys(input)).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Real-world scenarios', () => {
|
||||
it('should handle typical ComfyUI-Manager response with mixed enabled/disabled packs', () => {
|
||||
// Simulating actual server response pattern
|
||||
const serverResponse = {
|
||||
// Enabled packs come without version suffix
|
||||
'ComfyUI-Essential': { ver: '1.2.3', enabled: true, aux_id: undefined },
|
||||
'ComfyUI-Impact': { ver: '2.0.0', enabled: true, aux_id: undefined },
|
||||
// Disabled packs come with version suffix
|
||||
'ComfyUI-GGUF@1_1_4': {
|
||||
ver: '1.1.4',
|
||||
enabled: false,
|
||||
aux_id: undefined
|
||||
},
|
||||
'ComfyUI-Manager@2_5_0': {
|
||||
ver: '2.5.0',
|
||||
enabled: false,
|
||||
aux_id: undefined
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = normalizePackKeys(serverResponse)
|
||||
|
||||
// All keys should be normalized (no version suffixes)
|
||||
expect(Object.keys(normalized)).toEqual([
|
||||
'ComfyUI-Essential',
|
||||
'ComfyUI-Impact',
|
||||
'ComfyUI-GGUF',
|
||||
'ComfyUI-Manager'
|
||||
])
|
||||
|
||||
// Values should be preserved
|
||||
expect(normalized['ComfyUI-GGUF']).toEqual({
|
||||
ver: '1.1.4',
|
||||
enabled: false,
|
||||
aux_id: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow consistent access by pack ID regardless of enabled state', () => {
|
||||
const packsBeforeToggle = {
|
||||
'my-pack': { ver: '1.0.0', enabled: true }
|
||||
}
|
||||
|
||||
const packsAfterToggle = {
|
||||
'my-pack@1_0_0': { ver: '1.0.0', enabled: false }
|
||||
}
|
||||
|
||||
const normalizedBefore = normalizePackKeys(packsBeforeToggle)
|
||||
const normalizedAfter = normalizePackKeys(packsAfterToggle)
|
||||
|
||||
// Both should have the same key after normalization
|
||||
expect(normalizedBefore['my-pack']).toBeDefined()
|
||||
expect(normalizedAfter['my-pack']).toBeDefined()
|
||||
|
||||
// Can access by the same key regardless of the original format
|
||||
expect(Object.keys(normalizedBefore)).toEqual(
|
||||
Object.keys(normalizedAfter)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
86
src/utils/searchAndReplace.test.ts
Normal file
86
src/utils/searchAndReplace.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { applyTextReplacements } from '@/utils/searchAndReplace'
|
||||
|
||||
describe('applyTextReplacements', () => {
|
||||
// Test specifically the filename sanitization part
|
||||
describe('filename sanitization', () => {
|
||||
it('should replace invalid filename characters with underscores', () => {
|
||||
// Mock the minimal app structure needed
|
||||
const mockNodes = [
|
||||
{
|
||||
title: 'TestNode',
|
||||
widgets: [
|
||||
{
|
||||
name: 'testWidget',
|
||||
value:
|
||||
'file/name?with<invalid>chars\\:*|"control\x00chars\x1F\x7F'
|
||||
}
|
||||
]
|
||||
} as LGraphNode
|
||||
]
|
||||
|
||||
const mockGraph = new LGraph()
|
||||
for (const node of mockNodes) {
|
||||
mockGraph.add(node)
|
||||
}
|
||||
const result = applyTextReplacements(mockGraph, '%TestNode.testWidget%')
|
||||
|
||||
// The expected result should have all invalid characters replaced with underscores
|
||||
expect(result).toBe('file_name_with_invalid_chars_____control_chars__')
|
||||
})
|
||||
|
||||
it('should handle various invalid filename characters individually', () => {
|
||||
const testCases = [
|
||||
{ input: '/', expected: '_' },
|
||||
{ input: '?', expected: '_' },
|
||||
{ input: '<', expected: '_' },
|
||||
{ input: '>', expected: '_' },
|
||||
{ input: '\\', expected: '_' },
|
||||
{ input: ':', expected: '_' },
|
||||
{ input: '*', expected: '_' },
|
||||
{ input: '|', expected: '_' },
|
||||
{ input: '"', expected: '_' },
|
||||
{ input: '\x00', expected: '_' }, // NULL character
|
||||
{ input: '\x1F', expected: '_' }, // Unit separator
|
||||
{ input: '\x7F', expected: '_' } // Delete character
|
||||
]
|
||||
|
||||
for (const { input, expected } of testCases) {
|
||||
const mockNodes = [
|
||||
{
|
||||
title: 'TestNode',
|
||||
widgets: [{ name: 'testWidget', value: input }]
|
||||
} as LGraphNode
|
||||
]
|
||||
|
||||
const mockGraph = new LGraph()
|
||||
for (const node of mockNodes) {
|
||||
mockGraph.add(node)
|
||||
}
|
||||
const result = applyTextReplacements(mockGraph, '%TestNode.testWidget%')
|
||||
expect(result).toBe(expected)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not modify valid filename characters', () => {
|
||||
const validChars = 'abcABC123.-_ '
|
||||
|
||||
const mockNodes = [
|
||||
{
|
||||
title: 'TestNode',
|
||||
widgets: [{ name: 'testWidget', value: validChars }]
|
||||
} as LGraphNode
|
||||
]
|
||||
|
||||
const mockGraph = new LGraph()
|
||||
for (const node of mockNodes) {
|
||||
mockGraph.add(node)
|
||||
}
|
||||
const result = applyTextReplacements(mockGraph, '%TestNode.testWidget%')
|
||||
expect(result).toBe(validChars)
|
||||
})
|
||||
})
|
||||
})
|
||||
147
src/utils/treeUtil.test.ts
Normal file
147
src/utils/treeUtil.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { TreeNode } from '@/types/treeExplorerTypes'
|
||||
import { buildTree, sortedTree } from '@/utils/treeUtil'
|
||||
|
||||
describe('buildTree', () => {
|
||||
it('should handle empty folder items correctly', () => {
|
||||
const items = [
|
||||
{ path: 'a/b/c/' },
|
||||
{ path: 'a/b/d.txt' },
|
||||
{ path: 'a/e/' },
|
||||
{ path: 'f.txt' }
|
||||
]
|
||||
|
||||
const tree = buildTree(items, (item) => item.path.split('/'))
|
||||
|
||||
expect(tree).toEqual({
|
||||
key: 'root',
|
||||
label: 'root',
|
||||
children: [
|
||||
{
|
||||
key: 'root/a',
|
||||
label: 'a',
|
||||
leaf: false,
|
||||
children: [
|
||||
{
|
||||
key: 'root/a/b',
|
||||
label: 'b',
|
||||
leaf: false,
|
||||
children: [
|
||||
{
|
||||
key: 'root/a/b/c',
|
||||
label: 'c',
|
||||
leaf: false,
|
||||
children: [],
|
||||
data: { path: 'a/b/c/' }
|
||||
},
|
||||
{
|
||||
key: 'root/a/b/d.txt',
|
||||
label: 'd.txt',
|
||||
leaf: true,
|
||||
children: [],
|
||||
data: { path: 'a/b/d.txt' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'root/a/e',
|
||||
label: 'e',
|
||||
leaf: false,
|
||||
children: [],
|
||||
data: { path: 'a/e/' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'root/f.txt',
|
||||
label: 'f.txt',
|
||||
leaf: true,
|
||||
children: [],
|
||||
data: { path: 'f.txt' }
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('sortedTree', () => {
|
||||
const createNode = (label: string, leaf = false): TreeNode => ({
|
||||
key: label,
|
||||
label,
|
||||
leaf,
|
||||
children: []
|
||||
})
|
||||
|
||||
it('should return a new node instance', () => {
|
||||
const node = createNode('root')
|
||||
const result = sortedTree(node)
|
||||
expect(result).not.toBe(node)
|
||||
expect(result).toEqual(node)
|
||||
})
|
||||
|
||||
it('should sort children by label', () => {
|
||||
const node: TreeNode = {
|
||||
key: 'root',
|
||||
label: 'root',
|
||||
leaf: false,
|
||||
children: [createNode('c'), createNode('a'), createNode('b')]
|
||||
}
|
||||
|
||||
const result = sortedTree(node)
|
||||
expect(result.children?.map((c) => c.label)).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
describe('with groupLeaf=true', () => {
|
||||
it('should group folders before files', () => {
|
||||
const node: TreeNode = {
|
||||
key: 'root',
|
||||
label: 'root',
|
||||
children: [
|
||||
createNode('file.txt', true),
|
||||
createNode('folder1'),
|
||||
createNode('another.txt', true),
|
||||
createNode('folder2')
|
||||
]
|
||||
}
|
||||
|
||||
const result = sortedTree(node, { groupLeaf: true })
|
||||
const labels = result.children?.map((c) => c.label)
|
||||
expect(labels).toEqual(['folder1', 'folder2', 'another.txt', 'file.txt'])
|
||||
})
|
||||
|
||||
it('should sort recursively', () => {
|
||||
const node: TreeNode = {
|
||||
key: 'root',
|
||||
label: 'root',
|
||||
children: [
|
||||
{
|
||||
...createNode('folder1'),
|
||||
children: [
|
||||
createNode('z.txt', true),
|
||||
createNode('subfolder2'),
|
||||
createNode('a.txt', true),
|
||||
createNode('subfolder1')
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const result = sortedTree(node, { groupLeaf: true })
|
||||
const folder = result.children?.[0]
|
||||
const subLabels = folder?.children?.map((c) => c.label)
|
||||
expect(subLabels).toEqual(['subfolder1', 'subfolder2', 'a.txt', 'z.txt'])
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle nodes without children', () => {
|
||||
const node: TreeNode = {
|
||||
key: 'leaf',
|
||||
label: 'leaf',
|
||||
leaf: true
|
||||
}
|
||||
|
||||
const result = sortedTree(node)
|
||||
expect(result).toEqual(node)
|
||||
})
|
||||
})
|
||||
45
src/utils/typeGuardUtil.test.ts
Normal file
45
src/utils/typeGuardUtil.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isSubgraphIoNode } from '@/utils/typeGuardUtil'
|
||||
|
||||
describe('typeGuardUtil', () => {
|
||||
describe('isSubgraphIoNode', () => {
|
||||
it('should identify SubgraphInputNode as IO node', () => {
|
||||
const node = {
|
||||
constructor: { comfyClass: 'SubgraphInputNode' }
|
||||
} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(true)
|
||||
})
|
||||
|
||||
it('should identify SubgraphOutputNode as IO node', () => {
|
||||
const node = {
|
||||
constructor: { comfyClass: 'SubgraphOutputNode' }
|
||||
} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not identify regular nodes as IO nodes', () => {
|
||||
const node = {
|
||||
constructor: { comfyClass: 'CLIPTextEncode' }
|
||||
} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle nodes without constructor', () => {
|
||||
const node = {} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle nodes without comfyClass', () => {
|
||||
const node = {
|
||||
constructor: {}
|
||||
} as any
|
||||
|
||||
expect(isSubgraphIoNode(node)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user