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:
Alexander Brown
2026-03-05 16:22:33 -08:00
parent 2205ead595
commit 4796154884
7 changed files with 401 additions and 0 deletions

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

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

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

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

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

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

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