mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
test: add unit tests for pure utility modules
- strings.ts: parseSlotTypes, nextUniqueName (11 tests) - type.ts: commonType, isColorable, isNodeBindable, toClass (18 tests) - categoryLabel.ts: formatCategoryLabel (9 tests) - createAnnotatedPath.ts: string and ResultItem inputs (9 tests) - conflictMessageUtil.ts: getConflictMessage, getJoinedConflictMessages (7 tests)
This commit is contained in:
55
src/lib/litegraph/src/strings.test.ts
Normal file
55
src/lib/litegraph/src/strings.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { nextUniqueName, parseSlotTypes } from '@/lib/litegraph/src/strings'
|
||||
|
||||
describe('parseSlotTypes', () => {
|
||||
it('returns ["*"] for empty string', () => {
|
||||
expect(parseSlotTypes('')).toEqual(['*'])
|
||||
})
|
||||
|
||||
it('returns ["*"] for "0"', () => {
|
||||
expect(parseSlotTypes('0')).toEqual(['*'])
|
||||
})
|
||||
|
||||
it('returns ["*"] for numeric 0', () => {
|
||||
expect(parseSlotTypes(0)).toEqual(['*'])
|
||||
})
|
||||
|
||||
it('lowercases a single type', () => {
|
||||
expect(parseSlotTypes('IMAGE')).toEqual(['image'])
|
||||
})
|
||||
|
||||
it('splits comma-delimited types and lowercases each', () => {
|
||||
expect(parseSlotTypes('INT,FLOAT,STRING')).toEqual([
|
||||
'int',
|
||||
'float',
|
||||
'string'
|
||||
])
|
||||
})
|
||||
|
||||
it('passes through already lowercase types unchanged', () => {
|
||||
expect(parseSlotTypes('latent')).toEqual(['latent'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('nextUniqueName', () => {
|
||||
it('returns the original name when there are no conflicts', () => {
|
||||
expect(nextUniqueName('foo', ['bar', 'baz'])).toBe('foo')
|
||||
})
|
||||
|
||||
it('appends _1 when the name already exists', () => {
|
||||
expect(nextUniqueName('foo', ['foo'])).toBe('foo_1')
|
||||
})
|
||||
|
||||
it('appends _2 when both name and name_1 exist', () => {
|
||||
expect(nextUniqueName('foo', ['foo', 'foo_1'])).toBe('foo_2')
|
||||
})
|
||||
|
||||
it('returns the original name with an empty existingNames array', () => {
|
||||
expect(nextUniqueName('foo', [])).toBe('foo')
|
||||
})
|
||||
|
||||
it('returns the original name when existingNames is omitted', () => {
|
||||
expect(nextUniqueName('foo')).toBe('foo')
|
||||
})
|
||||
})
|
||||
111
src/lib/litegraph/src/utils/type.test.ts
Normal file
111
src/lib/litegraph/src/utils/type.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
commonType,
|
||||
isColorable,
|
||||
isNodeBindable,
|
||||
toClass
|
||||
} from '@/lib/litegraph/src/utils/type'
|
||||
|
||||
describe('toClass', () => {
|
||||
class Point {
|
||||
x: number
|
||||
y: number
|
||||
constructor(source: { x: number; y: number }) {
|
||||
this.x = source.x
|
||||
this.y = source.y
|
||||
}
|
||||
}
|
||||
|
||||
it('returns the existing instance unchanged when already the right class', () => {
|
||||
const instance = new Point({ x: 1, y: 2 })
|
||||
expect(toClass(Point, instance)).toBe(instance)
|
||||
})
|
||||
|
||||
it('creates a new instance from a plain object', () => {
|
||||
const plain = { x: 3, y: 4 }
|
||||
const result = toClass(Point, plain)
|
||||
expect(result).toBeInstanceOf(Point)
|
||||
expect(result.x).toBe(3)
|
||||
expect(result.y).toBe(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isColorable', () => {
|
||||
it('returns true for an object with both setColorOption and getColorOption', () => {
|
||||
const obj = {
|
||||
setColorOption: () => {},
|
||||
getColorOption: () => null
|
||||
}
|
||||
expect(isColorable(obj)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for null', () => {
|
||||
expect(isColorable(null)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when setColorOption is missing', () => {
|
||||
const obj = { getColorOption: () => null }
|
||||
expect(isColorable(obj)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when getColorOption is missing', () => {
|
||||
const obj = { setColorOption: () => {} }
|
||||
expect(isColorable(obj)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isNodeBindable', () => {
|
||||
it('returns true for an object with a setNodeId function', () => {
|
||||
const widget = { setNodeId: (_id: number) => {} }
|
||||
expect(isNodeBindable(widget)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when setNodeId is not a function', () => {
|
||||
const widget = { setNodeId: 42 }
|
||||
expect(isNodeBindable(widget)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for null', () => {
|
||||
expect(isNodeBindable(null)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('commonType', () => {
|
||||
it('returns the type when both arguments are identical', () => {
|
||||
expect(commonType('IMAGE', 'IMAGE')).toBe('IMAGE')
|
||||
})
|
||||
|
||||
it('returns undefined when two different types have no overlap', () => {
|
||||
expect(commonType('IMAGE', 'LATENT')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns the concrete type when one argument is a wildcard', () => {
|
||||
expect(commonType('*', 'IMAGE')).toBe('IMAGE')
|
||||
expect(commonType('IMAGE', '*')).toBe('IMAGE')
|
||||
})
|
||||
|
||||
it('returns wildcard when all arguments are wildcards', () => {
|
||||
expect(commonType('*', '*')).toBe('*')
|
||||
})
|
||||
|
||||
it('returns the intersection of comma-delimited type lists', () => {
|
||||
expect(commonType('IMAGE,LATENT', 'LATENT,MASK')).toBe('LATENT')
|
||||
})
|
||||
|
||||
it('returns multiple shared types joined by comma', () => {
|
||||
expect(commonType('IMAGE,LATENT,MASK', 'IMAGE,LATENT')).toBe('IMAGE,LATENT')
|
||||
})
|
||||
|
||||
it('returns undefined when comma-delimited lists have no overlap', () => {
|
||||
expect(commonType('IMAGE,MASK', 'LATENT,CONDITIONING')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for non-string input', () => {
|
||||
expect(commonType(42 as unknown as string)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('ignores wildcards and uses only the non-wildcard types in intersection', () => {
|
||||
expect(commonType('*', 'IMAGE,LATENT', 'LATENT')).toBe('LATENT')
|
||||
})
|
||||
})
|
||||
41
src/platform/assets/utils/categoryLabel.test.ts
Normal file
41
src/platform/assets/utils/categoryLabel.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { formatCategoryLabel } from '@/platform/assets/utils/categoryLabel'
|
||||
|
||||
describe('formatCategoryLabel', () => {
|
||||
it('returns "Models" for undefined input', () => {
|
||||
expect(formatCategoryLabel(undefined)).toBe('Models')
|
||||
})
|
||||
|
||||
it('returns "Models" for empty string', () => {
|
||||
expect(formatCategoryLabel('')).toBe('Models')
|
||||
})
|
||||
|
||||
it('returns "Diffusion" for the special case "diffusion_models"', () => {
|
||||
expect(formatCategoryLabel('diffusion_models')).toBe('Diffusion')
|
||||
})
|
||||
|
||||
it('capitalizes regular words joined by underscores', () => {
|
||||
expect(formatCategoryLabel('text_encoder')).toBe('Text Encoder')
|
||||
})
|
||||
|
||||
it('preserves the VAE acronym', () => {
|
||||
expect(formatCategoryLabel('vae')).toBe('VAE')
|
||||
})
|
||||
|
||||
it('preserves the CLIP acronym', () => {
|
||||
expect(formatCategoryLabel('clip')).toBe('CLIP')
|
||||
})
|
||||
|
||||
it('preserves the GLIGEN acronym', () => {
|
||||
expect(formatCategoryLabel('gligen')).toBe('GLIGEN')
|
||||
})
|
||||
|
||||
it('handles mixed acronym and regular word', () => {
|
||||
expect(formatCategoryLabel('clip_vision')).toBe('CLIP Vision')
|
||||
})
|
||||
|
||||
it('capitalizes a single word', () => {
|
||||
expect(formatCategoryLabel('checkpoints')).toBe('Checkpoints')
|
||||
})
|
||||
})
|
||||
68
src/utils/createAnnotatedPath.test.ts
Normal file
68
src/utils/createAnnotatedPath.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ResultItem } from '@/schemas/apiSchema'
|
||||
import { createAnnotatedPath } from '@/utils/createAnnotatedPath'
|
||||
|
||||
describe('createAnnotatedPath', () => {
|
||||
describe('string input', () => {
|
||||
it('returns filename unchanged with default options', () => {
|
||||
expect(createAnnotatedPath('image.png')).toBe('image.png')
|
||||
})
|
||||
|
||||
it('prepends subfolder when provided', () => {
|
||||
expect(createAnnotatedPath('image.png', { subfolder: 'sub' })).toBe(
|
||||
'sub/image.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('appends annotation for non-input rootFolder', () => {
|
||||
expect(createAnnotatedPath('image.png', { rootFolder: 'output' })).toBe(
|
||||
'image.png [output]'
|
||||
)
|
||||
})
|
||||
|
||||
it('adds no annotation when rootFolder is input', () => {
|
||||
expect(createAnnotatedPath('image.png', { rootFolder: 'input' })).toBe(
|
||||
'image.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not double-annotate an already-annotated filepath', () => {
|
||||
expect(
|
||||
createAnnotatedPath('image.png [output]', { rootFolder: 'temp' })
|
||||
).toBe('image.png [output]')
|
||||
})
|
||||
|
||||
it('combines subfolder and non-input rootFolder annotation', () => {
|
||||
expect(
|
||||
createAnnotatedPath('image.png', {
|
||||
subfolder: 'sub',
|
||||
rootFolder: 'temp'
|
||||
})
|
||||
).toBe('sub/image.png [temp]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ResultItem input', () => {
|
||||
it('combines filename and subfolder from ResultItem', () => {
|
||||
const item: ResultItem = { filename: 'photo.jpg', subfolder: 'gallery' }
|
||||
expect(createAnnotatedPath(item)).toBe('gallery/photo.jpg')
|
||||
})
|
||||
|
||||
it('appends annotation when ResultItem type is not input', () => {
|
||||
const item: ResultItem = {
|
||||
filename: 'result.png',
|
||||
subfolder: 'results',
|
||||
type: 'output'
|
||||
}
|
||||
expect(createAnnotatedPath(item, { rootFolder: 'output' })).toBe(
|
||||
'results/result.png [output]'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns just filename when subfolder is empty', () => {
|
||||
const item: ResultItem = { filename: 'solo.png', subfolder: '' }
|
||||
expect(createAnnotatedPath(item)).toBe('solo.png')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
|
||||
import {
|
||||
getConflictMessage,
|
||||
getJoinedConflictMessages
|
||||
} from '@/workbench/extensions/manager/utils/conflictMessageUtil'
|
||||
|
||||
function mockT(key: string, params?: Record<string, unknown>): string {
|
||||
if (params) {
|
||||
return `${key}|current=${params.current},required=${params.required}`
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
function makeConflict(
|
||||
type: string,
|
||||
current?: string,
|
||||
required?: string
|
||||
): ConflictDetail {
|
||||
return {
|
||||
type,
|
||||
current_value: current,
|
||||
required_value: required
|
||||
} as unknown as ConflictDetail
|
||||
}
|
||||
|
||||
describe('getConflictMessage', () => {
|
||||
it('returns interpolated message for comfyui_version conflict', () => {
|
||||
const result = getConflictMessage(
|
||||
makeConflict('comfyui_version', '1.0', '2.0'),
|
||||
mockT
|
||||
)
|
||||
expect(result).toBe(
|
||||
'manager.conflicts.conflictMessages.comfyui_version|current=1.0,required=2.0'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns interpolated message for frontend_version conflict', () => {
|
||||
const result = getConflictMessage(
|
||||
makeConflict('frontend_version', '1.0', '2.0'),
|
||||
mockT
|
||||
)
|
||||
expect(result).toContain('frontend_version')
|
||||
})
|
||||
|
||||
it('returns simple message for banned conflict', () => {
|
||||
const result = getConflictMessage(makeConflict('banned'), mockT)
|
||||
expect(result).toBe('manager.conflicts.conflictMessages.banned')
|
||||
})
|
||||
|
||||
it('returns simple message for pending conflict', () => {
|
||||
const result = getConflictMessage(makeConflict('pending'), mockT)
|
||||
expect(result).toBe('manager.conflicts.conflictMessages.pending')
|
||||
})
|
||||
|
||||
it('returns generic message for unknown conflict type', () => {
|
||||
const result = getConflictMessage(
|
||||
makeConflict('unknown_type', 'a', 'b'),
|
||||
mockT
|
||||
)
|
||||
expect(result).toContain('generic')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getJoinedConflictMessages', () => {
|
||||
it('joins multiple conflict messages with default separator', () => {
|
||||
const conflicts = [makeConflict('banned'), makeConflict('pending')]
|
||||
const result = getJoinedConflictMessages(conflicts, mockT)
|
||||
expect(result).toBe(
|
||||
'manager.conflicts.conflictMessages.banned; manager.conflicts.conflictMessages.pending'
|
||||
)
|
||||
})
|
||||
|
||||
it('uses custom separator', () => {
|
||||
const conflicts = [makeConflict('banned'), makeConflict('pending')]
|
||||
const result = getJoinedConflictMessages(conflicts, mockT, ' | ')
|
||||
expect(result).toContain(' | ')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user