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:
Alexander Brown
2026-03-03 14:26:21 -08:00
parent 82ba0028fb
commit 8799d4be07
5 changed files with 356 additions and 0 deletions

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

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

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

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

View File

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