mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
test: add unit tests for untested utility modules
Cover resourceUrl, queueDisplay, objectUrlUtil, gridUtil, keyCombo, keybinding, and promotedWidgetTypes with 45 tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
20
src/core/graph/subgraph/promotedWidgetTypes.test.ts
Normal file
20
src/core/graph/subgraph/promotedWidgetTypes.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isPromotedWidgetView } from './promotedWidgetTypes'
|
||||
|
||||
describe('isPromotedWidgetView', () => {
|
||||
it('returns true for widgets with sourceNodeId and sourceWidgetName', () => {
|
||||
const widget = {
|
||||
name: 'test',
|
||||
value: 0,
|
||||
sourceNodeId: 'node-1',
|
||||
sourceWidgetName: 'steps'
|
||||
}
|
||||
expect(isPromotedWidgetView(widget as never)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for regular widgets', () => {
|
||||
const widget = { name: 'test', value: 0 }
|
||||
expect(isPromotedWidgetView(widget as never)).toBe(false)
|
||||
})
|
||||
})
|
||||
80
src/platform/keybindings/keyCombo.test.ts
Normal file
80
src/platform/keybindings/keyCombo.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { KeyComboImpl } from './keyCombo'
|
||||
|
||||
describe('KeyComboImpl', () => {
|
||||
it('creates from KeyCombo object with defaults', () => {
|
||||
const combo = new KeyComboImpl({ key: 'a' })
|
||||
expect(combo.key).toBe('a')
|
||||
expect(combo.ctrl).toBe(false)
|
||||
expect(combo.alt).toBe(false)
|
||||
expect(combo.shift).toBe(false)
|
||||
})
|
||||
|
||||
it('serializes to deterministic string', () => {
|
||||
const combo = new KeyComboImpl({ key: 's', ctrl: true })
|
||||
expect(combo.serialize()).toBe('S:true:false:false')
|
||||
})
|
||||
|
||||
it('renders human-readable toString', () => {
|
||||
const combo = new KeyComboImpl({ key: 's', ctrl: true, shift: true })
|
||||
expect(combo.toString()).toBe('Ctrl + Shift + s')
|
||||
})
|
||||
|
||||
it('equals another KeyComboImpl (case-insensitive key)', () => {
|
||||
const a = new KeyComboImpl({ key: 'A', ctrl: true })
|
||||
const b = new KeyComboImpl({ key: 'a', ctrl: true })
|
||||
expect(a.equals(b)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not equal different combos', () => {
|
||||
const a = new KeyComboImpl({ key: 'a', ctrl: true })
|
||||
const b = new KeyComboImpl({ key: 'a', alt: true })
|
||||
expect(a.equals(b)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not equal non-KeyComboImpl objects', () => {
|
||||
const combo = new KeyComboImpl({ key: 'a' })
|
||||
expect(combo.equals({ key: 'a' })).toBe(false)
|
||||
expect(combo.equals(null)).toBe(false)
|
||||
})
|
||||
|
||||
it('detects modifier presence', () => {
|
||||
expect(new KeyComboImpl({ key: 'a' }).hasModifier).toBe(false)
|
||||
expect(new KeyComboImpl({ key: 'a', ctrl: true }).hasModifier).toBe(true)
|
||||
expect(new KeyComboImpl({ key: 'a', alt: true }).hasModifier).toBe(true)
|
||||
expect(new KeyComboImpl({ key: 'a', shift: true }).hasModifier).toBe(true)
|
||||
})
|
||||
|
||||
it('detects modifier keys', () => {
|
||||
expect(new KeyComboImpl({ key: 'Control' }).isModifier).toBe(true)
|
||||
expect(new KeyComboImpl({ key: 'a' }).isModifier).toBe(false)
|
||||
})
|
||||
|
||||
it('counts modifiers', () => {
|
||||
expect(
|
||||
new KeyComboImpl({ key: 'a', ctrl: true, shift: true }).modifierCount
|
||||
).toBe(2)
|
||||
})
|
||||
|
||||
it('detects shift-only combos', () => {
|
||||
expect(new KeyComboImpl({ key: 'a', shift: true }).isShiftOnly).toBe(true)
|
||||
expect(
|
||||
new KeyComboImpl({ key: 'a', shift: true, ctrl: true }).isShiftOnly
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('creates from keyboard event', () => {
|
||||
const event = {
|
||||
key: 'z',
|
||||
ctrlKey: true,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
metaKey: false
|
||||
} as KeyboardEvent
|
||||
|
||||
const combo = KeyComboImpl.fromEvent(event)
|
||||
expect(combo.key).toBe('z')
|
||||
expect(combo.ctrl).toBe(true)
|
||||
})
|
||||
})
|
||||
62
src/platform/keybindings/keybinding.test.ts
Normal file
62
src/platform/keybindings/keybinding.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { KeybindingImpl } from './keybinding'
|
||||
|
||||
describe('KeybindingImpl', () => {
|
||||
it('creates from Keybinding object', () => {
|
||||
const binding = new KeybindingImpl({
|
||||
commandId: 'save',
|
||||
combo: { key: 's', ctrl: true }
|
||||
})
|
||||
expect(binding.commandId).toBe('save')
|
||||
expect(binding.combo.key).toBe('s')
|
||||
expect(binding.combo.ctrl).toBe(true)
|
||||
})
|
||||
|
||||
it('equals another KeybindingImpl with same values', () => {
|
||||
const a = new KeybindingImpl({
|
||||
commandId: 'save',
|
||||
combo: { key: 's', ctrl: true }
|
||||
})
|
||||
const b = new KeybindingImpl({
|
||||
commandId: 'save',
|
||||
combo: { key: 's', ctrl: true }
|
||||
})
|
||||
expect(a.equals(b)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not equal binding with different command', () => {
|
||||
const a = new KeybindingImpl({
|
||||
commandId: 'save',
|
||||
combo: { key: 's', ctrl: true }
|
||||
})
|
||||
const b = new KeybindingImpl({
|
||||
commandId: 'open',
|
||||
combo: { key: 's', ctrl: true }
|
||||
})
|
||||
expect(a.equals(b)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not equal binding with different targetElementId', () => {
|
||||
const a = new KeybindingImpl({
|
||||
commandId: 'save',
|
||||
combo: { key: 's', ctrl: true },
|
||||
targetElementId: 'canvas'
|
||||
})
|
||||
const b = new KeybindingImpl({
|
||||
commandId: 'save',
|
||||
combo: { key: 's', ctrl: true },
|
||||
targetElementId: 'sidebar'
|
||||
})
|
||||
expect(a.equals(b)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not equal non-KeybindingImpl objects', () => {
|
||||
const binding = new KeybindingImpl({
|
||||
commandId: 'save',
|
||||
combo: { key: 's', ctrl: true }
|
||||
})
|
||||
expect(binding.equals(null)).toBe(false)
|
||||
expect(binding.equals({ commandId: 'save' })).toBe(false)
|
||||
})
|
||||
})
|
||||
29
src/utils/gridUtil.test.ts
Normal file
29
src/utils/gridUtil.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createGridStyle } from './gridUtil'
|
||||
|
||||
describe('createGridStyle', () => {
|
||||
it('returns default grid styles', () => {
|
||||
const style = createGridStyle()
|
||||
expect(style.display).toBe('grid')
|
||||
expect(style.gap).toBe('1rem')
|
||||
expect(style.padding).toBe('0')
|
||||
expect(style.gridTemplateColumns).toContain('auto-fill')
|
||||
})
|
||||
|
||||
it('uses fixed columns when specified', () => {
|
||||
const style = createGridStyle({ columns: 3 })
|
||||
expect(style.gridTemplateColumns).toBe('repeat(3, 1fr)')
|
||||
})
|
||||
|
||||
it('clamps columns to at least 1', () => {
|
||||
const style = createGridStyle({ columns: -1 })
|
||||
expect(style.gridTemplateColumns).toBe('repeat(1, 1fr)')
|
||||
})
|
||||
|
||||
it('applies custom minWidth and gap', () => {
|
||||
const style = createGridStyle({ minWidth: '20rem', gap: '2rem' })
|
||||
expect(style.gridTemplateColumns).toContain('20rem')
|
||||
expect(style.gap).toBe('2rem')
|
||||
})
|
||||
})
|
||||
46
src/utils/objectUrlUtil.test.ts
Normal file
46
src/utils/objectUrlUtil.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
createSharedObjectUrl,
|
||||
releaseSharedObjectUrl,
|
||||
retainSharedObjectUrl
|
||||
} from './objectUrlUtil'
|
||||
|
||||
describe('objectUrlUtil', () => {
|
||||
it('creates and releases an object URL', () => {
|
||||
const revokespy = vi.spyOn(URL, 'revokeObjectURL')
|
||||
const url = createSharedObjectUrl(new Blob(['test']))
|
||||
expect(url).toMatch(/^blob:/)
|
||||
|
||||
releaseSharedObjectUrl(url)
|
||||
expect(revokespy).toHaveBeenCalledWith(url)
|
||||
revokespy.mockRestore()
|
||||
})
|
||||
|
||||
it('retains and releases with ref counting', () => {
|
||||
const revokespy = vi.spyOn(URL, 'revokeObjectURL')
|
||||
const url = createSharedObjectUrl(new Blob(['test']))
|
||||
|
||||
retainSharedObjectUrl(url)
|
||||
releaseSharedObjectUrl(url)
|
||||
expect(revokespy).not.toHaveBeenCalled()
|
||||
|
||||
releaseSharedObjectUrl(url)
|
||||
expect(revokespy).toHaveBeenCalledWith(url)
|
||||
revokespy.mockRestore()
|
||||
})
|
||||
|
||||
it('ignores non-blob URLs', () => {
|
||||
const revokespy = vi.spyOn(URL, 'revokeObjectURL')
|
||||
|
||||
retainSharedObjectUrl('https://example.com')
|
||||
releaseSharedObjectUrl('https://example.com')
|
||||
expect(revokespy).not.toHaveBeenCalled()
|
||||
|
||||
retainSharedObjectUrl(undefined)
|
||||
releaseSharedObjectUrl(undefined)
|
||||
expect(revokespy).not.toHaveBeenCalled()
|
||||
|
||||
revokespy.mockRestore()
|
||||
})
|
||||
})
|
||||
111
src/utils/queueDisplay.test.ts
Normal file
111
src/utils/queueDisplay.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { JobState } from '@/types/queue'
|
||||
|
||||
import { buildJobDisplay, iconForJobState } from './queueDisplay'
|
||||
import type { BuildJobDisplayCtx } from './queueDisplay'
|
||||
|
||||
describe('iconForJobState', () => {
|
||||
it.each([
|
||||
['pending', 'icon-[lucide--loader-circle]'],
|
||||
['initialization', 'icon-[lucide--server-crash]'],
|
||||
['running', 'icon-[lucide--zap]'],
|
||||
['completed', 'icon-[lucide--check-check]'],
|
||||
['failed', 'icon-[lucide--alert-circle]']
|
||||
] as [JobState, string][])(
|
||||
'returns correct icon for %s state',
|
||||
(state, expected) => {
|
||||
expect(iconForJobState(state)).toBe(expected)
|
||||
}
|
||||
)
|
||||
|
||||
it('returns default icon for unknown state', () => {
|
||||
expect(iconForJobState('unknown' as JobState)).toBe('icon-[lucide--circle]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildJobDisplay', () => {
|
||||
const mockT = (key: string, vars?: Record<string, unknown>) => {
|
||||
if (vars) return `${key}:${JSON.stringify(vars)}`
|
||||
return key
|
||||
}
|
||||
|
||||
const mockFormatClock = (ts: number) => `clock:${ts}`
|
||||
|
||||
function makeTask(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
jobId: 'abc-123',
|
||||
createTime: 1000,
|
||||
executionTimeInSeconds: 2.5,
|
||||
executionTime: 2500,
|
||||
previewOutput: null,
|
||||
job: { priority: 1 },
|
||||
...overrides
|
||||
} as never
|
||||
}
|
||||
|
||||
function makeCtx(
|
||||
overrides: Partial<BuildJobDisplayCtx> = {}
|
||||
): BuildJobDisplayCtx {
|
||||
return {
|
||||
t: mockT,
|
||||
locale: 'en',
|
||||
formatClockTimeFn: mockFormatClock,
|
||||
isActive: false,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
it('returns queued display for pending state', () => {
|
||||
const result = buildJobDisplay(makeTask(), 'pending', makeCtx())
|
||||
expect(result.primary).toBe('queue.inQueue')
|
||||
expect(result.showClear).toBe(true)
|
||||
})
|
||||
|
||||
it('returns added hint for pending with showAddedHint', () => {
|
||||
const result = buildJobDisplay(
|
||||
makeTask(),
|
||||
'pending',
|
||||
makeCtx({ showAddedHint: true })
|
||||
)
|
||||
expect(result.primary).toBe('queue.jobAddedToQueue')
|
||||
expect(result.iconName).toBe('icon-[lucide--check]')
|
||||
})
|
||||
|
||||
it('returns initialization display', () => {
|
||||
const result = buildJobDisplay(makeTask(), 'initialization', makeCtx())
|
||||
expect(result.primary).toBe('queue.initializingAlmostReady')
|
||||
})
|
||||
|
||||
it('returns running display with progress when active', () => {
|
||||
const result = buildJobDisplay(
|
||||
makeTask(),
|
||||
'running',
|
||||
makeCtx({
|
||||
isActive: true,
|
||||
totalPercent: 50,
|
||||
currentNodePercent: 75,
|
||||
currentNodeName: 'KSampler'
|
||||
})
|
||||
)
|
||||
expect(result.primary).toContain('sideToolbar.queueProgressOverlay.total')
|
||||
expect(result.secondary).toContain('KSampler')
|
||||
})
|
||||
|
||||
it('returns simple running display when not active', () => {
|
||||
const result = buildJobDisplay(makeTask(), 'running', makeCtx())
|
||||
expect(result.primary).toBe('g.running')
|
||||
})
|
||||
|
||||
it('returns completed display with execution time', () => {
|
||||
const result = buildJobDisplay(makeTask(), 'completed', makeCtx())
|
||||
expect(result.secondary).toBe('2.50s')
|
||||
expect(result.showClear).toBe(false)
|
||||
})
|
||||
|
||||
it('returns failed display', () => {
|
||||
const result = buildJobDisplay(makeTask(), 'failed', makeCtx())
|
||||
expect(result.primary).toBe('g.failed')
|
||||
expect(result.showClear).toBe(true)
|
||||
})
|
||||
})
|
||||
53
src/utils/resourceUrl.test.ts
Normal file
53
src/utils/resourceUrl.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
getRandParam: () => '&rand=12345'
|
||||
}
|
||||
}))
|
||||
|
||||
import { getResourceURL, splitFilePath } from './resourceUrl'
|
||||
|
||||
describe('splitFilePath', () => {
|
||||
it('returns empty subfolder for filename without path', () => {
|
||||
expect(splitFilePath('image.png')).toEqual(['', 'image.png'])
|
||||
})
|
||||
|
||||
it('splits path into subfolder and filename', () => {
|
||||
expect(splitFilePath('models/checkpoints/model.safetensors')).toEqual([
|
||||
'models/checkpoints',
|
||||
'model.safetensors'
|
||||
])
|
||||
})
|
||||
|
||||
it('handles single directory level', () => {
|
||||
expect(splitFilePath('subfolder/file.txt')).toEqual([
|
||||
'subfolder',
|
||||
'file.txt'
|
||||
])
|
||||
})
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(splitFilePath('')).toEqual(['', ''])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getResourceURL', () => {
|
||||
it('builds URL with default type', () => {
|
||||
const url = getResourceURL('models', 'model.safetensors')
|
||||
expect(url).toContain('/view?')
|
||||
expect(url).toContain('filename=model.safetensors')
|
||||
expect(url).toContain('type=input')
|
||||
expect(url).toContain('subfolder=models')
|
||||
})
|
||||
|
||||
it('builds URL with custom type', () => {
|
||||
const url = getResourceURL('outputs', 'result.png', 'output')
|
||||
expect(url).toContain('type=output')
|
||||
})
|
||||
|
||||
it('encodes special characters in filename', () => {
|
||||
const url = getResourceURL('', 'file with spaces.png')
|
||||
expect(url).toContain('filename=file%20with%20spaces.png')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user